<commit_before>import { Float32BufferAttribute } from '../Three';
import { LineSegmentsGeometry } from '../core/LineSegmentsGeometry';
import { LineSegments2 } from '../core/LineSegments2';
import { STATION_XSECT } from '../core/constants';
import { Segments } from './Segments';

class Legs extends LineSegments2 {

	constructor ( ctx ) {

		const geometry = new LineSegmentsGeometry();

		super( geometry, ctx.materials.getSurveyLineMaterial( 'basic' ) );

		this.ctx = ctx;
		this.colourCache = ctx.materials.colourCache;
		this.legLengths = [];
		this.legVertices = [];
		this.legToSegment = []; // maps vertex index to segment membership
		this.colors = [];
		this.type = 'Legs';
		this.highlightLeg = null;
		this.highlightSegment = null;
		this.scale.set( 1, 1, 1 );
		this.pathsSet = false;

	}

	addLegs ( survey, vertices, legRuns ) {

		this.legVertices = vertices;
		this.legRuns = legRuns;

		const positions = new Float32BufferAttribute( vertices.length * 3, 3 );
		const colors = new Float32BufferAttribute( vertices.length * 3, 3 );

		positions.copyVector3sArray( vertices );
		colors.array.fill( 1.0 );

		const geometry = this.geometry;

		geometry.setPositions( positions.array );
		geometry.setColors( colors.array );

		this.computeLineDistances();
		this.computeStats( survey );

		this.colors = colors;

		return this;

	}

	computeStats ( survey ) {

		const vertices = this.legVertices;
		const l = vertices.length;

		const n = l / 2;
		const legLengths = new Array( n );

		let s1 = 0, s2 = 0;
		let min = Infinity;
		let max = -Infinity;

		for ( let i = 0; i < l; i += 2 ) {

			const v1 = vertices[ i ];
			const v2 = vertices[ i + 1 ];

			const legLength = survey.getGeographicalDistance( v1, v2 );

			legLengths[ i / 2 ] = legLength; // cache lengths to avoid recalc

			s1 += legLength;
			s2 += legLength * legLength;

			max = Math.max( max, legLength );
			min = Math.min( min, legLength );

		}

		const stats = {
			minLegLength: min,
			maxLegLength: max,
			legLength: s1,
			legLengthSD: Math.sqrt( s2 / n - Math.pow( s1 / n, 2 ) ),
			legLengthRange: max - min,
			legCount: n
		};

		this.legLengths = legLengths;
		this.stats = stats;

		return this;

	}

	cutRuns ( survey, selection ) {

		const idSet = selection.getIds();
		const legRuns = this.legRuns;

		if ( ! legRuns ) return;

		const vertices = this.legVertices;

		const newVertices = [];
		const newLegRuns = [];

		const l = legRuns.length;

		for ( let run = 0; run < l; run++ ) {

			const legRun = legRuns[ run ];

			const survey = legRun.survey;
			const start  = legRun.start;
			const end    = legRun.end;

			if ( idSet.has( survey ) ) {

				let vp = newVertices.length;

				for ( let v = start; v < end; v++ ) {

					const station = vertices[ v ];

					newVertices.push( station );

					//  clear topology info
					station.legs = [];
					station.linkedSegments = [];

				}

				// adjust vertex run for new vertices and color arrays

				legRun.start = vp;

				vp += end - start;

				legRun.end = vp;

				newLegRuns.push( legRun );

			}

		}

		if ( newVertices.length === 0 ) return false;

		this.geometry.dispose();

		this.addLegs( survey, newVertices, newLegRuns );

		return true;

	}

	setHighlightLeg ( l ) {

		this.highlightLeg = l;

	}

	setHighlightSegment ( l ) {

		this.highlightSegment = l;

	}

	setShading ( idSet, colourSegment, mode, dashed, filterConnected ) {

		this.material = this.ctx.materials.getSurveyLineMaterial( mode, dashed );
		this.material.needsUpdate = true;

		const legRuns = this.legRuns;
		const unselectedColor = this.ctx.cfg.themeColor( 'shading.unselected' );

		const vertices = this.legVertices;
		const colors = this.colors.array;
		const highlightLeg = this.highlightLeg === null ? null : this.highlightLeg * 2;
		const highlightSegment = this.highlightSegment;

		if ( idSet.size > 0 && legRuns ) {

			for ( let run = 0, l = legRuns.length; run < l; run++ ) {

				const legRun = legRuns[ run ];

				const survey = legRun.survey;
				const start  = legRun.start;
				const end    = legRun.end;

				if ( idSet.has( survey ) ) {

					for ( let v = start; v < end; v += 2 ) {

						colourSegment( vertices, colors, v, v + 1, survey );

					}

				} else {

					for ( let v = start; v < end; v++ ) {

						unselectedColor.toArray( colors, v * 3 );

					}

				}

			}

		} else {

			const segments = this.legToSegment;

			for ( let v1 = 0, l = vertices.length; v1 < l; v1 += 2 ) {

				const v2 = v1 + 1;

				if (
					( highlightLeg !== null && v1 !== highlightLeg ) ||
					( highlightSegment !== null && segments[ v1 / 2 ] !== highlightSegment ) ||
					( filterConnected && ( vertices[ v1 ].shortestPath === Infinity || vertices[ v2 ].shortestPath === Infinity ) )
				) {

					unselectedColor.toArray( colors, v1 * 3 );
					unselectedColor.toArray( colors, v2 * 3 );

				} else {

					colourSegment( vertices, colors, v1, v2, null );

				}

			}

		}

		this.geometry.getAttribute( 'instanceColorStart' ).needsUpdate = true;
		this.geometry.getAttribute( 'instanceColorEnd' ).needsUpdate = true;

	}

	hide ( mode ) {

		if ( mode ) {

			const vertices = this.legVertices;
			const legCount = vertices.length / 2;

			const hide = new Float32Array( legCount );

			for ( let i = 0; i < legCount; i++ ) {

				const sType1 = vertices[ i * 2 ].type;
				const sType2 = vertices[ i * 2 + 1 ].type;

				hide[ i ] = sType1 & STATION_XSECT && sType2 & STATION_XSECT ? 1 : 0;

			}

			this.geometry.setHide( hide );

		} else {

			this.geometry.clearHide();

		}

	}

	vertexSegment ( index ) {

		return this.legToSegment[ index / 2 ];

	}

	getLegInfo ( legIndex ) {

		const vertices = this.legVertices;
		const vertexIndex = legIndex * 2;

		return {
			index: legIndex,
			start: vertices[ vertexIndex ],
			end: vertices[ vertexIndex + 1 ],
			segment: this.legToSegment[ legIndex ],
			length: this.legLengths[ legIndex ]
		};

	}

	setLegColor ( leg, color1, color2 = null ) {

		const c1 = this.colourCache.getColour( color1 );
		const c2 = ( ! color2 ) ? c1 : this.colourCache.getColour( color2 );

		const colours = this.colors.array;

		leg *= 3;
		c1.toArray( colours, leg );
		c2.toArray( colours, leg + 3 );

		this.geometry.getAttribute( 'instanceColorStart' ).needsUpdate = true;
		this.geometry.getAttribute( 'instanceColorEnd' ).needsUpdate = true;

	}

	findTopology () {

		// determine segments between junctions and entrances/passage ends and create mapping array.

		const legs = this.legVertices;
		const legLengths = this.legLengths;
		const segments = new Segments();

		const l = legs.length;

		this.legToSegment = new Array( l / 2 );

		const legToSegment = this.legToSegment;

		let station;
		let newSegment = true;
		let segment = 0;
		let segmentInfo;

		for ( let i = 0; i < l; i = i + 2 ) {

			const v1 = legs[ i ];
			const v2 = legs[ i + 1 ];

			legToSegment[ i / 2 ] = segment;

			station = v1;

			if ( station !== undefined ) {

				station.legs.push( i );
				station.linkedSegments.push( segment );

			}

			if ( newSegment ) {

				if ( station === undefined ) continue; // possible use of separator in station name.

				segmentInfo = {
					segment: segment,
					startStation: station,
					endStation: null,
					length: 0
				};

				newSegment = false;

			}

			segmentInfo.length += legLengths[ i / 2 ];

			station = v2;

			if ( station !== undefined ) station.legs.push( i );

			if ( station && ( station.connections > 2 || ( i + 2 < l && ! station.equals( legs[ i + 2 ] ) ) ) ) {

				// we have found a junction or a passage end

				_addSegment();

				segment++;
				newSegment = true;

			}

		}

		if ( ! newSegment ) {

			_addSegment();

		}

		return segments;

		function _addSegment() {

			segmentInfo.endStation = station;

			segments.addSegment( segmentInfo );

			station.linkedSegments.push( segment );

		}

	}

	getAdjacentStations ( station ) {

		const legs = this.legVertices;
		const adjacentLegs = station.legs;
		const thisVertex = station;
		const ids = [];

		if ( ! adjacentLegs ) return ids;

		adjacentLegs.forEach( l => {

			const v1 = legs[ l ];
			const nextVertex = ( v1 === thisVertex ) ? legs[ l + 1 ] : v1;

			ids.push( nextVertex.id );

		} );

		return ids;

	}

	setShortestPaths ( station, legCallback = null ) {

		// queue of stations searched.
		const queue = [ station ];

		const legs = this.legVertices;
		const legLengths = this.legLengths;
		const legsSeen = [];

		let maxDistance = 0;

		station.shortestPath = 0;

		while ( queue.length > 0 ) {

			const station = queue.shift();
			const stationLegs = station.legs;

			const currentDistance = station.shortestPath;

			maxDistance = Math.max( maxDistance, currentDistance );

			// find stations connected to this station
			for ( let i = 0; i < stationLegs.length; i++ ) {

				const leg = stationLegs[ i ];

				const v1 = legs[ leg ];

				const legIndex = leg / 2;
				const nextStation = ( v1 === station ) ? legs[ leg + 1 ] : v1;
				const nextLength = legLengths[ legIndex];

				if ( legCallback !== null && ! legsSeen[ leg ] ) {

					legCallback( this.getLegInfo( legIndex ) );
					legsSeen[ leg ] = true;

				}

				// label stations with distance of shortest path
				// add to search list

				if ( nextStation.shortestPath > currentDistance + nextLength ) {

					nextStation.shortestPath = currentDistance + nextLength;
					queue.push( nextStation );

				}

			}

		}

		this.pathsSet = true;

		return maxDistance;

	}

	getShortestPath ( startStation ) {

		const path = new Set();

		let shortestPath = startStation.shortestPath;

		if (
			! this.pathsSet ||
			shortestPath === Infinity ||
			shortestPath === 0
		) return path;

		const legs = this.legVertices;

		_shortestPathSearch( null, startStation );

		return path;

		function _shortestPathSearch ( lastStation, station ) {

			const stationLegs = station.legs;
			const l = stationLegs.length;

			for ( let i = 0; i < l; i++ ) {

				const leg = stationLegs[ i ];
				const v1 = legs[ leg ];

				const nextStation = ( v1 === station ) ? legs[ leg + 1 ] : v1;

				// prevent loops with zero length legs
				if ( nextStation === lastStation ) continue;

				// '<=' to search via zero length legs
				if ( nextStation.shortestPath <= shortestPath ) {

					shortestPath = nextStation.shortestPath;

					path.add( leg );

					if ( nextStation.shortestPath === 0 ) {

						return;

					} else {

						_shortestPathSearch( station, nextStation );

					}

				}

			}

		}

	}

	forEachLeg ( callback ) {

		const l = this.legLengths.length;

		for ( let i = 0; i < l; i++ ) {
			callback ( this.getLegInfo( i ) );
		}

	}

}

export { Legs };<commit_msg>reset station data in cut operation<commit_after>import { Float32BufferAttribute } from '../Three';
import { LineSegmentsGeometry } from '../core/LineSegmentsGeometry';
import { LineSegments2 } from '../core/LineSegments2';
import { STATION_XSECT } from '../core/constants';
import { Segments } from './Segments';

class Legs extends LineSegments2 {

	constructor ( ctx ) {

		const geometry = new LineSegmentsGeometry();

		super( geometry, ctx.materials.getSurveyLineMaterial( 'basic' ) );

		this.ctx = ctx;
		this.colourCache = ctx.materials.colourCache;
		this.legLengths = [];
		this.legVertices = [];
		this.legToSegment = []; // maps vertex index to segment membership
		this.colors = [];
		this.type = 'Legs';
		this.highlightLeg = null;
		this.highlightSegment = null;
		this.scale.set( 1, 1, 1 );
		this.pathsSet = false;

	}

	addLegs ( survey, vertices, legRuns ) {

		this.legVertices = vertices;
		this.legRuns = legRuns;

		const positions = new Float32BufferAttribute( vertices.length * 3, 3 );
		const colors = new Float32BufferAttribute( vertices.length * 3, 3 );

		positions.copyVector3sArray( vertices );
		colors.array.fill( 1.0 );

		const geometry = this.geometry;

		geometry.setPositions( positions.array );
		geometry.setColors( colors.array );

		this.computeLineDistances();
		this.computeStats( survey );

		this.colors = colors;

		return this;

	}

	computeStats ( survey ) {

		const vertices = this.legVertices;
		const l = vertices.length;

		const n = l / 2;
		const legLengths = new Array( n );

		let s1 = 0, s2 = 0;
		let min = Infinity;
		let max = -Infinity;

		for ( let i = 0; i < l; i += 2 ) {

			const v1 = vertices[ i ];
			const v2 = vertices[ i + 1 ];

			const legLength = survey.getGeographicalDistance( v1, v2 );

			legLengths[ i / 2 ] = legLength; // cache lengths to avoid recalc

			s1 += legLength;
			s2 += legLength * legLength;

			max = Math.max( max, legLength );
			min = Math.min( min, legLength );

		}

		const stats = {
			minLegLength: min,
			maxLegLength: max,
			legLength: s1,
			legLengthSD: Math.sqrt( s2 / n - Math.pow( s1 / n, 2 ) ),
			legLengthRange: max - min,
			legCount: n
		};

		this.legLengths = legLengths;
		this.stats = stats;

		return this;

	}

	cutRuns ( survey, selection ) {

		const idSet = selection.getIds();
		const legRuns = this.legRuns;

		if ( ! legRuns ) return;

		const vertices = this.legVertices;

		const newVertices = [];
		const newLegRuns = [];

		const l = legRuns.length;

		for ( let run = 0; run < l; run++ ) {

			const legRun = legRuns[ run ];

			const survey = legRun.survey;
			const start  = legRun.start;
			const end    = legRun.end;

			if ( idSet.has( survey ) ) {

				let vp = newVertices.length;

				for ( let v = start; v < end; v++ ) {

					const station = vertices[ v ];

					newVertices.push( station );

					//  clear topology info
					station.legs = [];
					station.linkedSegments = [];

					station.stationVertexIndex = -1;
					station.shortestPath = Infinity;

				}

				// adjust vertex run for new vertices and color arrays

				legRun.start = vp;

				vp += end - start;

				legRun.end = vp;

				newLegRuns.push( legRun );

			}

		}

		if ( newVertices.length === 0 ) return false;

		this.geometry.dispose();

		this.addLegs( survey, newVertices, newLegRuns );

		return true;

	}

	setHighlightLeg ( l ) {

		this.highlightLeg = l;

	}

	setHighlightSegment ( l ) {

		this.highlightSegment = l;

	}

	setShading ( idSet, colourSegment, mode, dashed, filterConnected ) {

		this.material = this.ctx.materials.getSurveyLineMaterial( mode, dashed );
		this.material.needsUpdate = true;

		const legRuns = this.legRuns;
		const unselectedColor = this.ctx.cfg.themeColor( 'shading.unselected' );

		const vertices = this.legVertices;
		const colors = this.colors.array;
		const highlightLeg = this.highlightLeg === null ? null : this.highlightLeg * 2;
		const highlightSegment = this.highlightSegment;

		if ( idSet.size > 0 && legRuns ) {

			for ( let run = 0, l = legRuns.length; run < l; run++ ) {

				const legRun = legRuns[ run ];

				const survey = legRun.survey;
				const start  = legRun.start;
				const end    = legRun.end;

				if ( idSet.has( survey ) ) {

					for ( let v = start; v < end; v += 2 ) {

						colourSegment( vertices, colors, v, v + 1, survey );

					}

				} else {

					for ( let v = start; v < end; v++ ) {

						unselectedColor.toArray( colors, v * 3 );

					}

				}

			}

		} else {

			const segments = this.legToSegment;

			for ( let v1 = 0, l = vertices.length; v1 < l; v1 += 2 ) {

				const v2 = v1 + 1;

				if (
					( highlightLeg !== null && v1 !== highlightLeg ) ||
					( highlightSegment !== null && segments[ v1 / 2 ] !== highlightSegment ) ||
					( filterConnected && ( vertices[ v1 ].shortestPath === Infinity || vertices[ v2 ].shortestPath === Infinity ) )
				) {

					unselectedColor.toArray( colors, v1 * 3 );
					unselectedColor.toArray( colors, v2 * 3 );

				} else {

					colourSegment( vertices, colors, v1, v2, null );

				}

			}

		}

		this.geometry.getAttribute( 'instanceColorStart' ).needsUpdate = true;
		this.geometry.getAttribute( 'instanceColorEnd' ).needsUpdate = true;

	}

	hide ( mode ) {

		if ( mode ) {

			const vertices = this.legVertices;
			const legCount = vertices.length / 2;

			const hide = new Float32Array( legCount );

			for ( let i = 0; i < legCount; i++ ) {

				const sType1 = vertices[ i * 2 ].type;
				const sType2 = vertices[ i * 2 + 1 ].type;

				hide[ i ] = sType1 & STATION_XSECT && sType2 & STATION_XSECT ? 1 : 0;

			}

			this.geometry.setHide( hide );

		} else {

			this.geometry.clearHide();

		}

	}

	vertexSegment ( index ) {

		return this.legToSegment[ index / 2 ];

	}

	getLegInfo ( legIndex ) {

		const vertices = this.legVertices;
		const vertexIndex = legIndex * 2;

		return {
			index: legIndex,
			start: vertices[ vertexIndex ],
			end: vertices[ vertexIndex + 1 ],
			segment: this.legToSegment[ legIndex ],
			length: this.legLengths[ legIndex ]
		};

	}

	setLegColor ( leg, color1, color2 = null ) {

		const c1 = this.colourCache.getColour( color1 );
		const c2 = ( ! color2 ) ? c1 : this.colourCache.getColour( color2 );

		const colours = this.colors.array;

		leg *= 3;
		c1.toArray( colours, leg );
		c2.toArray( colours, leg + 3 );

		this.geometry.getAttribute( 'instanceColorStart' ).needsUpdate = true;
		this.geometry.getAttribute( 'instanceColorEnd' ).needsUpdate = true;

	}

	findTopology () {

		// determine segments between junctions and entrances/passage ends and create mapping array.

		const legs = this.legVertices;
		const legLengths = this.legLengths;
		const segments = new Segments();

		const l = legs.length;

		this.legToSegment = new Array( l / 2 );

		const legToSegment = this.legToSegment;

		let station;
		let newSegment = true;
		let segment = 0;
		let segmentInfo;

		for ( let i = 0; i < l; i = i + 2 ) {

			const v1 = legs[ i ];
			const v2 = legs[ i + 1 ];

			legToSegment[ i / 2 ] = segment;

			station = v1;

			if ( station !== undefined ) {

				station.legs.push( i );
				station.linkedSegments.push( segment );

			}

			if ( newSegment ) {

				if ( station === undefined ) continue; // possible use of separator in station name.

				segmentInfo = {
					segment: segment,
					startStation: station,
					endStation: null,
					length: 0
				};

				newSegment = false;

			}

			segmentInfo.length += legLengths[ i / 2 ];

			station = v2;

			if ( station !== undefined ) station.legs.push( i );

			if ( station && ( station.connections > 2 || ( i + 2 < l && ! station.equals( legs[ i + 2 ] ) ) ) ) {

				// we have found a junction or a passage end

				_addSegment();

				segment++;
				newSegment = true;

			}

		}

		if ( ! newSegment ) {

			_addSegment();

		}

		return segments;

		function _addSegment() {

			segmentInfo.endStation = station;

			segments.addSegment( segmentInfo );

			station.linkedSegments.push( segment );

		}

	}

	getAdjacentStations ( station ) {

		const legs = this.legVertices;
		const adjacentLegs = station.legs;
		const thisVertex = station;
		const ids = [];

		if ( ! adjacentLegs ) return ids;

		adjacentLegs.forEach( l => {

			const v1 = legs[ l ];
			const nextVertex = ( v1 === thisVertex ) ? legs[ l + 1 ] : v1;

			ids.push( nextVertex.id );

		} );

		return ids;

	}

	setShortestPaths ( station, legCallback = null ) {

		// queue of stations searched.
		const queue = [ station ];

		const legs = this.legVertices;
		const legLengths = this.legLengths;
		const legsSeen = [];

		let maxDistance = 0;

		station.shortestPath = 0;

		while ( queue.length > 0 ) {

			const station = queue.shift();
			const stationLegs = station.legs;

			const currentDistance = station.shortestPath;

			maxDistance = Math.max( maxDistance, currentDistance );

			// find stations connected to this station
			for ( let i = 0; i < stationLegs.length; i++ ) {

				const leg = stationLegs[ i ];

				const v1 = legs[ leg ];

				const legIndex = leg / 2;
				const nextStation = ( v1 === station ) ? legs[ leg + 1 ] : v1;
				const nextLength = legLengths[ legIndex];

				if ( legCallback !== null && ! legsSeen[ leg ] ) {

					legCallback( this.getLegInfo( legIndex ) );
					legsSeen[ leg ] = true;

				}

				// label stations with distance of shortest path
				// add to search list

				if ( nextStation.shortestPath > currentDistance + nextLength ) {

					nextStation.shortestPath = currentDistance + nextLength;
					queue.push( nextStation );

				}

			}

		}

		this.pathsSet = true;

		return maxDistance;

	}

	getShortestPath ( startStation ) {

		const path = new Set();

		let shortestPath = startStation.shortestPath;

		if (
			! this.pathsSet ||
			shortestPath === Infinity ||
			shortestPath === 0
		) return path;

		const legs = this.legVertices;

		_shortestPathSearch( null, startStation );

		return path;

		function _shortestPathSearch ( lastStation, station ) {

			const stationLegs = station.legs;
			const l = stationLegs.length;

			for ( let i = 0; i < l; i++ ) {

				const leg = stationLegs[ i ];
				const v1 = legs[ leg ];

				const nextStation = ( v1 === station ) ? legs[ leg + 1 ] : v1;

				// prevent loops with zero length legs
				if ( nextStation === lastStation ) continue;

				// '<=' to search via zero length legs
				if ( nextStation.shortestPath <= shortestPath ) {

					shortestPath = nextStation.shortestPath;

					path.add( leg );

					if ( nextStation.shortestPath === 0 ) {

						return;

					} else {

						_shortestPathSearch( station, nextStation );

					}

				}

			}

		}

	}

	forEachLeg ( callback ) {

		const l = this.legLengths.length;

		for ( let i = 0; i < l; i++ ) {
			callback ( this.getLegInfo( i ) );
		}

	}

}

export { Legs };<|endoftext|>


<commit_before>/* global __phantom_writeFile */
(function(global) {
    var UNDEFINED,
        exportObject;

    if (typeof module !== "undefined" && module.exports) {
        exportObject = exports;
    } else {
        exportObject = global.jasmineReporters = global.jasmineReporters || {};
    }

    function trim(str) { return str.replace(/^\s+/, "" ).replace(/\s+$/, "" ); }
    function elapsed(start, end) { return (end - start)/1000; }
    function isFailed(obj) { return obj.status === "failed"; }
    function isSkipped(obj) { return obj.status === "pending"; }
    function isDisabled(obj) { return obj.status === "disabled"; }
    function pad(n) { return n < 10 ? '0'+n : n; }
    function extend(dupe, obj) { // performs a shallow copy of all props of `obj` onto `dupe`
        for (var prop in obj) {
            if (obj.hasOwnProperty(prop)) {
                dupe[prop] = obj[prop];
            }
        }
        return dupe;
    }
    function ISODateString(d) {
        return d.getFullYear() + '-' +
            pad(d.getMonth()+1) + '-' +
            pad(d.getDate()) + 'T' +
            pad(d.getHours()) + ':' +
            pad(d.getMinutes()) + ':' +
            pad(d.getSeconds());
    }
    function escapeInvalidXmlChars(str) {
        return str.replace(/\&/g, "&")
            .replace(/</g, "<")
            .replace(/\>/g, ">")
            .replace(/\"/g, """)
            .replace(/\'/g, "'");
    }
    function getQualifiedFilename(path, filename, separator) {
        if (path && path.substr(-1) !== separator && filename.substr(0) !== separator) {
            path += separator;
        }
        return path + filename;
    }
    function log(str) {
        var con = global.console || console;
        if (con && con.log) {
            con.log(str);
        }
    }

    /**
     * A delegate for letting the consumer
     * modify the suite name when it is used inside the junit report and as a file
     * name. This is useful when running a test suite against multiple capabilities
     * because the report can have unique names for each combination of suite/spec
     * and capability/test environment.
     *
     * @callback modifySuiteName
     * @param {string} fullName
     * @param {object} suite
     */

    /**
     * Generates JUnit XML for the given spec run. There are various options
     * to control where the results are written, and the default values are
     * set to create as few .xml files as possible. It is possible to save a
     * single XML file, or an XML file for each top-level `describe`, or an
     * XML file for each `describe` regardless of nesting.
     *
     * Usage:
     *
     * jasmine.getEnv().addReporter(new jasmineReporters.JUnitXmlReporter(options));
     *
     * @param {object} [options]
     * @param {string} [savePath] directory to save the files (default: '')
     * @param {boolean} [consolidateAll] whether to save all test results in a
     *   single file (default: true)
     *   NOTE: if true, {filePrefix} is treated as the full filename (excluding
     *     extension)
     * @param {boolean} [consolidate] whether to save nested describes within the
     *   same file as their parent (default: true)
     *   NOTE: true does nothing if consolidateAll is also true.
     *   NOTE: false also sets consolidateAll to false.
     * @param {boolean} [useDotNotation] whether to separate suite names with
     *   dots instead of spaces, ie "Class.init" not "Class init" (default: true)
     * @param {string} [filePrefix] is the string value that is prepended to the
     *   xml output file (default: junitresults-)
     *   NOTE: if consolidateAll is true, the default is simply "junitresults" and
     *     this becomes the actual filename, ie "junitresults.xml"
     * @param {string} [package] is the base package for all test suits that are
     *   handled by this report {default: none}
     * @param {modifySuiteName} [modifySuiteName] a delegate for letting the consumer
     *   modify the suite name when it is used inside the junit report and as a file
     *   name. This is useful when running a test suite against multiple capabilities
     *   because the report can have unique names for each combination of suite/spec
     *   and capability/test environment.
     */
    exportObject.JUnitXmlReporter = function(options) {
        var self = this;
        self.started = false;
        self.finished = false;
        // sanitize arguments
        options = options || {};
        self.savePath = options.savePath || '';
        self.consolidate = options.consolidate === UNDEFINED ? true : options.consolidate;
        self.consolidateAll = self.consolidate !== false && (options.consolidateAll === UNDEFINED ? true : options.consolidateAll);
        self.useDotNotation = options.useDotNotation === UNDEFINED ? true : options.useDotNotation;
        if (self.consolidateAll) {
            self.filePrefix = options.filePrefix || 'junitresults';
        } else {
            self.filePrefix = typeof options.filePrefix === 'string' ? options.filePrefix : 'junitresults-';
        }
        self.package = typeof(options.package) === 'string' ? escapeInvalidXmlChars(options.package) : UNDEFINED;

        if(options.modifySuiteName && typeof options.modifySuiteName !== 'function') {
            throw new Error('option "modifySuiteName" must be a function');
        }

        var delegates = {};
        delegates.modifySuiteName = options.modifySuiteName;

        var suites = [],
            currentSuite = null,
            totalSpecsExecuted = 0,
            totalSpecsDefined,
            // when use use fit, jasmine never calls suiteStarted / suiteDone, so make a fake one to use
            fakeFocusedSuite = {
                id: 'focused',
                description: 'focused specs',
                fullName: 'focused specs'
            };

        var __suites = {}, __specs = {};
        function getSuite(suite) {
            __suites[suite.id] = extend(__suites[suite.id] || {}, suite);
            return __suites[suite.id];
        }
        function getSpec(spec) {
            __specs[spec.id] = extend(__specs[spec.id] || {}, spec);
            return __specs[spec.id];
        }

        self.jasmineStarted = function(summary) {
            totalSpecsDefined = summary && summary.totalSpecsDefined || NaN;
            exportObject.startTime = new Date();
            self.started = true;
        };
        self.suiteStarted = function(suite) {
            suite = getSuite(suite);
            suite._startTime = new Date();
            suite._specs = [];
            suite._suites = [];
            suite._failures = 0;
            suite._skipped = 0;
            suite._disabled = 0;
            suite._parent = currentSuite;
            if (!currentSuite) {
                suites.push(suite);
            } else {
                currentSuite._suites.push(suite);
            }
            currentSuite = suite;
        };
        self.specStarted = function(spec) {
            if (!currentSuite) {
                // focused spec (fit) -- suiteStarted was never called
                self.suiteStarted(fakeFocusedSuite);
            }
            spec = getSpec(spec);
            spec._startTime = new Date();
            spec._suite = currentSuite;
            currentSuite._specs.push(spec);
        };
        self.specDone = function(spec) {
            spec = getSpec(spec);
            spec._endTime = new Date();
            if (isSkipped(spec)) { spec._suite._skipped++; }
            if (isDisabled(spec)) { spec._suite._disabled++; }
            if (isFailed(spec)) { spec._suite._failures += spec.failedExpectations.length; }
            totalSpecsExecuted++;
        };
        self.suiteDone = function(suite) {
            suite = getSuite(suite);
            if (suite._parent === UNDEFINED) {
                // disabled suite (xdescribe) -- suiteStarted was never called
                self.suiteStarted(suite);
            }
            suite._endTime = new Date();
            currentSuite = suite._parent;
        };
        self.jasmineDone = function() {
            if (currentSuite) {
                // focused spec (fit) -- suiteDone was never called
                self.suiteDone(fakeFocusedSuite);
            }
            var output = '';
            for (var i = 0; i < suites.length; i++) {
                output += self.getOrWriteNestedOutput(suites[i]);
            }
            // if we have anything to write here, write out the consolidated file
            if (output) {
                wrapOutputAndWriteFile(self.filePrefix, output);
            }
            //log("Specs skipped but not reported (entire suite skipped or targeted to specific specs)", totalSpecsDefined - totalSpecsExecuted + totalSpecsDisabled);

            self.finished = true;
            // this is so phantomjs-testrunner.js can tell if we're done executing
            exportObject.endTime = new Date();
        };

        self.getOrWriteNestedOutput = function(suite) {
            var output = suiteAsXml(suite);
            for (var i = 0; i < suite._suites.length; i++) {
                output += self.getOrWriteNestedOutput(suite._suites[i]);
            }
            if (self.consolidateAll || self.consolidate && suite._parent) {
                return output;
            } else {
                // if we aren't supposed to consolidate output, just write it now
                wrapOutputAndWriteFile(generateFilename(suite), output);
                return '';
            }
        };

        self.writeFile = function(filename, text) {
            var errors = [];
            var path = self.savePath;

            function phantomWrite(path, filename, text) {
                // turn filename into a qualified path
                filename = getQualifiedFilename(path, filename, window.fs_path_separator);
                // write via a method injected by phantomjs-testrunner.js
                __phantom_writeFile(filename, text);
            }

            function nodeWrite(path, filename, text) {
                var fs = require("fs");
                var nodejs_path = require("path");
                require("mkdirp").sync(path); // make sure the path exists
                var filepath = nodejs_path.join(path, filename);
                var xmlfile = fs.openSync(filepath, "w");
                fs.writeSync(xmlfile, text, 0);
                fs.closeSync(xmlfile);
                return;
            }
            // Attempt writing with each possible environment.
            // Track errors in case no write succeeds
            try {
                phantomWrite(path, filename, text);
                return;
            } catch (e) { errors.push('  PhantomJs attempt: ' + e.message); }
            try {
                nodeWrite(path, filename, text);
                return;
            } catch (f) { errors.push('  NodeJS attempt: ' + f.message); }

            // If made it here, no write succeeded.  Let user know.
            log("Warning: writing junit report failed for '" + path + "', '" +
                filename + "'. Reasons:\n" +
                errors.join("\n")
            );
        };

        /******** Helper functions with closure access for simplicity ********/
        function generateFilename(suite) {
            return self.filePrefix + getFullyQualifiedSuiteName(suite, true) + '.xml';
        }

        function getFullyQualifiedSuiteName(suite, isFilename) {
            var fullName;
            if (self.useDotNotation || isFilename) {
                fullName = suite.description;
                for (var parent = suite._parent; parent; parent = parent._parent) {
                    fullName = parent.description + '.' + fullName;
                }
            } else {
                fullName = suite.fullName;
            }

            // Either remove or escape invalid XML characters
            if (isFilename) {
                var fileName = "",
                    rFileChars = /[\w\.]/,
                    chr;
                while (fullName.length) {
                    chr = fullName[0];
                    fullName = fullName.substr(1);
                    if (rFileChars.test(chr)) {
                        fileName += chr;
                    }
                }
                return fileName;
            } else {

                if(delegates.modifySuiteName) {
                    fullName = options.modifySuiteName(fullName, suite);
                }

                return escapeInvalidXmlChars(fullName);
            }
        }

        function suiteAsXml(suite) {
            var xml = '\n <testsuite name="' + getFullyQualifiedSuiteName(suite) + '"';
            xml += ' timestamp="' + ISODateString(suite._startTime) + '"';
            xml += ' hostname="localhost"'; // many CI systems like Jenkins don't care about this, but junit spec says it is required
            xml += ' time="' + elapsed(suite._startTime, suite._endTime) + '"';
            xml += ' errors="0"';
            xml += ' tests="' + suite._specs.length + '"';
            xml += ' skipped="' + suite._skipped + '"';
            xml += ' disabled="' + suite._disabled + '"';
            // Because of JUnit's flat structure, only include directly failed tests (not failures for nested suites)
            xml += ' failures="' + suite._failures + '"';
            if (self.package) {
                xml += ' package="' + self.package + '"';
            }
            xml += '>';

            for (var i = 0; i < suite._specs.length; i++) {
                xml += specAsXml(suite._specs[i]);
            }
            xml += '\n </testsuite>';
            return xml;
        }
        function specAsXml(spec) {
            var xml = '\n  <testcase classname="' + getFullyQualifiedSuiteName(spec._suite) + '"';
            xml += ' name="' + escapeInvalidXmlChars(spec.description) + '"';
            xml += ' time="' + elapsed(spec._startTime, spec._endTime) + '"';

            var testCaseBody = '';
            if (isSkipped(spec) || isDisabled(spec)) {
                if (spec.pendingReason) {
                    testCaseBody = '\n   <skipped message="' + trim(escapeInvalidXmlChars(spec.pendingReason)) + '" />';
                } else {
                    testCaseBody = '\n   <skipped />';
                }
            } else if (isFailed(spec)) {
                for (var i = 0, failure; i < spec.failedExpectations.length; i++) {
                    failure = spec.failedExpectations[i];
                    testCaseBody += '\n   <failure type="' + (failure.matcherName || "exception") + '"';
                    testCaseBody += ' message="' + trim(escapeInvalidXmlChars(failure.message))+ '"';
                    testCaseBody += '>';
                    testCaseBody += '<![CDATA[' + trim(failure.stack || failure.message) + ']]>';
                    testCaseBody += '\n   </failure>';
                }
            }

            if (testCaseBody) {
                xml += '>' + testCaseBody + '\n  </testcase>';
            } else {
                xml += ' />';
            }
            return xml;
        }

        // To remove complexity and be more DRY about the silly preamble and <testsuites> element
        var prefix = '<?xml version="1.0" encoding="UTF-8" ?>';
        prefix += '\n<testsuites>';
        var suffix = '\n</testsuites>';
        function wrapOutputAndWriteFile(filename, text) {
            if (filename.substr(-4) !== '.xml') { filename += '.xml'; }
            self.writeFile(filename, (prefix + text + suffix));
        }
    };
})(this);
<commit_msg>Adds system-out callback option<commit_after>/* global __phantom_writeFile */
(function(global) {
    var UNDEFINED,
        exportObject;

    if (typeof module !== "undefined" && module.exports) {
        exportObject = exports;
    } else {
        exportObject = global.jasmineReporters = global.jasmineReporters || {};
    }

    function trim(str) { return str.replace(/^\s+/, "" ).replace(/\s+$/, "" ); }
    function elapsed(start, end) { return (end - start)/1000; }
    function isFailed(obj) { return obj.status === "failed"; }
    function isSkipped(obj) { return obj.status === "pending"; }
    function isDisabled(obj) { return obj.status === "disabled"; }
    function pad(n) { return n < 10 ? '0'+n : n; }
    function extend(dupe, obj) { // performs a shallow copy of all props of `obj` onto `dupe`
        for (var prop in obj) {
            if (obj.hasOwnProperty(prop)) {
                dupe[prop] = obj[prop];
            }
        }
        return dupe;
    }
    function ISODateString(d) {
        return d.getFullYear() + '-' +
            pad(d.getMonth()+1) + '-' +
            pad(d.getDate()) + 'T' +
            pad(d.getHours()) + ':' +
            pad(d.getMinutes()) + ':' +
            pad(d.getSeconds());
    }
    function escapeInvalidXmlChars(str) {
        return str.replace(/\&/g, "&")
            .replace(/</g, "<")
            .replace(/\>/g, ">")
            .replace(/\"/g, """)
            .replace(/\'/g, "'");
    }
    function getQualifiedFilename(path, filename, separator) {
        if (path && path.substr(-1) !== separator && filename.substr(0) !== separator) {
            path += separator;
        }
        return path + filename;
    }
    function log(str) {
        var con = global.console || console;
        if (con && con.log) {
            con.log(str);
        }
    }

    /**
     * A delegate for letting the consumer
     * modify the suite name when it is used inside the junit report and as a file
     * name. This is useful when running a test suite against multiple capabilities
     * because the report can have unique names for each combination of suite/spec
     * and capability/test environment.
     *
     * @callback modifySuiteName
     * @param {string} fullName
     * @param {object} suite
     */

    /**
     * Generates JUnit XML for the given spec run. There are various options
     * to control where the results are written, and the default values are
     * set to create as few .xml files as possible. It is possible to save a
     * single XML file, or an XML file for each top-level `describe`, or an
     * XML file for each `describe` regardless of nesting.
     *
     * Usage:
     *
     * jasmine.getEnv().addReporter(new jasmineReporters.JUnitXmlReporter(options));
     *
     * @param {object} [options]
     * @param {string} [savePath] directory to save the files (default: '')
     * @param {boolean} [consolidateAll] whether to save all test results in a
     *   single file (default: true)
     *   NOTE: if true, {filePrefix} is treated as the full filename (excluding
     *     extension)
     * @param {boolean} [consolidate] whether to save nested describes within the
     *   same file as their parent (default: true)
     *   NOTE: true does nothing if consolidateAll is also true.
     *   NOTE: false also sets consolidateAll to false.
     * @param {boolean} [useDotNotation] whether to separate suite names with
     *   dots instead of spaces, ie "Class.init" not "Class init" (default: true)
     * @param {string} [filePrefix] is the string value that is prepended to the
     *   xml output file (default: junitresults-)
     *   NOTE: if consolidateAll is true, the default is simply "junitresults" and
     *     this becomes the actual filename, ie "junitresults.xml"
     * @param {string} [package] is the base package for all test suits that are
     *   handled by this report {default: none}
     * @param {modifySuiteName} [modifySuiteName] a delegate for letting the consumer
     *   modify the suite name when it is used inside the junit report and as a file
     *   name. This is useful when running a test suite against multiple capabilities
     *   because the report can have unique names for each combination of suite/spec
     *   and capability/test environment.
     */
    exportObject.JUnitXmlReporter = function(options) {
        var self = this;
        self.started = false;
        self.finished = false;
        // sanitize arguments
        options = options || {};
        self.savePath = options.savePath || '';
        self.consolidate = options.consolidate === UNDEFINED ? true : options.consolidate;
        self.consolidateAll = self.consolidate !== false && (options.consolidateAll === UNDEFINED ? true : options.consolidateAll);
        self.useDotNotation = options.useDotNotation === UNDEFINED ? true : options.useDotNotation;
        if (self.consolidateAll) {
            self.filePrefix = options.filePrefix || 'junitresults';
        } else {
            self.filePrefix = typeof options.filePrefix === 'string' ? options.filePrefix : 'junitresults-';
        }
        self.package = typeof(options.package) === 'string' ? escapeInvalidXmlChars(options.package) : UNDEFINED;

        if(options.modifySuiteName && typeof options.modifySuiteName !== 'function') {
            throw new Error('option "modifySuiteName" must be a function');
        }

        var delegates = {};
        delegates.modifySuiteName = options.modifySuiteName;
        delegates.systemOut = options.systemOut;

        var suites = [],
            currentSuite = null,
            totalSpecsExecuted = 0,
            totalSpecsDefined,
            // when use use fit, jasmine never calls suiteStarted / suiteDone, so make a fake one to use
            fakeFocusedSuite = {
                id: 'focused',
                description: 'focused specs',
                fullName: 'focused specs'
            };

        var __suites = {}, __specs = {};
        function getSuite(suite) {
            __suites[suite.id] = extend(__suites[suite.id] || {}, suite);
            return __suites[suite.id];
        }
        function getSpec(spec) {
            __specs[spec.id] = extend(__specs[spec.id] || {}, spec);
            return __specs[spec.id];
        }

        self.jasmineStarted = function(summary) {
            totalSpecsDefined = summary && summary.totalSpecsDefined || NaN;
            exportObject.startTime = new Date();
            self.started = true;
        };
        self.suiteStarted = function(suite) {
            suite = getSuite(suite);
            suite._startTime = new Date();
            suite._specs = [];
            suite._suites = [];
            suite._failures = 0;
            suite._skipped = 0;
            suite._disabled = 0;
            suite._parent = currentSuite;
            if (!currentSuite) {
                suites.push(suite);
            } else {
                currentSuite._suites.push(suite);
            }
            currentSuite = suite;
        };
        self.specStarted = function(spec) {
            if (!currentSuite) {
                // focused spec (fit) -- suiteStarted was never called
                self.suiteStarted(fakeFocusedSuite);
            }
            spec = getSpec(spec);
            spec._startTime = new Date();
            spec._suite = currentSuite;
            currentSuite._specs.push(spec);
        };
        self.specDone = function(spec) {
            spec = getSpec(spec);
            spec._endTime = new Date();
            if (isSkipped(spec)) { spec._suite._skipped++; }
            if (isDisabled(spec)) { spec._suite._disabled++; }
            if (isFailed(spec)) { spec._suite._failures += spec.failedExpectations.length; }
            totalSpecsExecuted++;
        };
        self.suiteDone = function(suite) {
            suite = getSuite(suite);
            if (suite._parent === UNDEFINED) {
                // disabled suite (xdescribe) -- suiteStarted was never called
                self.suiteStarted(suite);
            }
            suite._endTime = new Date();
            currentSuite = suite._parent;
        };
        self.jasmineDone = function() {
            if (currentSuite) {
                // focused spec (fit) -- suiteDone was never called
                self.suiteDone(fakeFocusedSuite);
            }
            var output = '';
            for (var i = 0; i < suites.length; i++) {
                output += self.getOrWriteNestedOutput(suites[i]);
            }
            // if we have anything to write here, write out the consolidated file
            if (output) {
                wrapOutputAndWriteFile(self.filePrefix, output);
            }
            //log("Specs skipped but not reported (entire suite skipped or targeted to specific specs)", totalSpecsDefined - totalSpecsExecuted + totalSpecsDisabled);

            self.finished = true;
            // this is so phantomjs-testrunner.js can tell if we're done executing
            exportObject.endTime = new Date();
        };

        self.getOrWriteNestedOutput = function(suite) {
            var output = suiteAsXml(suite);
            for (var i = 0; i < suite._suites.length; i++) {
                output += self.getOrWriteNestedOutput(suite._suites[i]);
            }
            if (self.consolidateAll || self.consolidate && suite._parent) {
                return output;
            } else {
                // if we aren't supposed to consolidate output, just write it now
                wrapOutputAndWriteFile(generateFilename(suite), output);
                return '';
            }
        };

        self.writeFile = function(filename, text) {
            var errors = [];
            var path = self.savePath;

            function phantomWrite(path, filename, text) {
                // turn filename into a qualified path
                filename = getQualifiedFilename(path, filename, window.fs_path_separator);
                // write via a method injected by phantomjs-testrunner.js
                __phantom_writeFile(filename, text);
            }

            function nodeWrite(path, filename, text) {
                var fs = require("fs");
                var nodejs_path = require("path");
                require("mkdirp").sync(path); // make sure the path exists
                var filepath = nodejs_path.join(path, filename);
                var xmlfile = fs.openSync(filepath, "w");
                fs.writeSync(xmlfile, text, 0);
                fs.closeSync(xmlfile);
                return;
            }
            // Attempt writing with each possible environment.
            // Track errors in case no write succeeds
            try {
                phantomWrite(path, filename, text);
                return;
            } catch (e) { errors.push('  PhantomJs attempt: ' + e.message); }
            try {
                nodeWrite(path, filename, text);
                return;
            } catch (f) { errors.push('  NodeJS attempt: ' + f.message); }

            // If made it here, no write succeeded.  Let user know.
            log("Warning: writing junit report failed for '" + path + "', '" +
                filename + "'. Reasons:\n" +
                errors.join("\n")
            );
        };

        /******** Helper functions with closure access for simplicity ********/
        function generateFilename(suite) {
            return self.filePrefix + getFullyQualifiedSuiteName(suite, true) + '.xml';
        }

        function getFullyQualifiedSuiteName(suite, isFilename) {
            var fullName;
            if (self.useDotNotation || isFilename) {
                fullName = suite.description;
                for (var parent = suite._parent; parent; parent = parent._parent) {
                    fullName = parent.description + '.' + fullName;
                }
            } else {
                fullName = suite.fullName;
            }

            // Either remove or escape invalid XML characters
            if (isFilename) {
                var fileName = "",
                    rFileChars = /[\w\.]/,
                    chr;
                while (fullName.length) {
                    chr = fullName[0];
                    fullName = fullName.substr(1);
                    if (rFileChars.test(chr)) {
                        fileName += chr;
                    }
                }
                return fileName;
            } else {

                if(delegates.modifySuiteName) {
                    fullName = options.modifySuiteName(fullName, suite);
                }

                return escapeInvalidXmlChars(fullName);
            }
        }

        function suiteAsXml(suite) {
            var xml = '\n <testsuite name="' + getFullyQualifiedSuiteName(suite) + '"';
            xml += ' timestamp="' + ISODateString(suite._startTime) + '"';
            xml += ' hostname="localhost"'; // many CI systems like Jenkins don't care about this, but junit spec says it is required
            xml += ' time="' + elapsed(suite._startTime, suite._endTime) + '"';
            xml += ' errors="0"';
            xml += ' tests="' + suite._specs.length + '"';
            xml += ' skipped="' + suite._skipped + '"';
            xml += ' disabled="' + suite._disabled + '"';
            // Because of JUnit's flat structure, only include directly failed tests (not failures for nested suites)
            xml += ' failures="' + suite._failures + '"';
            if (self.package) {
                xml += ' package="' + self.package + '"';
            }
            xml += '>';

            for (var i = 0; i < suite._specs.length; i++) {
                xml += specAsXml(suite._specs[i]);
            }
            xml += '\n </testsuite>';
            return xml;
        }
        function specAsXml(spec) {
            var xml = '\n  <testcase classname="' + getFullyQualifiedSuiteName(spec._suite) + '"';
            xml += ' name="' + escapeInvalidXmlChars(spec.description) + '"';
            xml += ' time="' + elapsed(spec._startTime, spec._endTime) + '"';

            var testCaseBody = '';
            if (isSkipped(spec) || isDisabled(spec)) {
                if (spec.pendingReason) {
                    testCaseBody = '\n   <skipped message="' + trim(escapeInvalidXmlChars(spec.pendingReason)) + '" />';
                } else {
                    testCaseBody = '\n   <skipped />';
                }
            } else if (isFailed(spec)) {
                for (var i = 0, failure; i < spec.failedExpectations.length; i++) {
                    failure = spec.failedExpectations[i];
                    testCaseBody += '\n   <failure type="' + (failure.matcherName || "exception") + '"';
                    testCaseBody += ' message="' + trim(escapeInvalidXmlChars(failure.message))+ '"';
                    testCaseBody += '>';
                    testCaseBody += '<![CDATA[' + trim(failure.stack || failure.message) + ']]>';
                    testCaseBody += '\n   </failure>';
                }
            }

            if (testCaseBody || delegates.systemOut) {
                xml += '>' + testCaseBody;
                if (delegates.systemOut) {
                    xml += '\n   <system-out>' + trim(escapeInvalidXmlChars(delegates.systemOut(spec, getFullyQualifiedSuiteName(spec._suite, true)))) + '</system-out>';
                }
                xml += '\n  </testcase>';
            } else {
                xml += ' />';
            }
            return xml;
        }

        // To remove complexity and be more DRY about the silly preamble and <testsuites> element
        var prefix = '<?xml version="1.0" encoding="UTF-8" ?>';
        prefix += '\n<testsuites>';
        var suffix = '\n</testsuites>';
        function wrapOutputAndWriteFile(filename, text) {
            if (filename.substr(-4) !== '.xml') { filename += '.xml'; }
            self.writeFile(filename, (prefix + text + suffix));
        }
    };
})(this);
<|endoftext|>


<commit_before>/*
 * MelonJS Game Engine
 * Copyright (C) 2011 - 2013, Olivier BIOT
 * http://www.melonjs.org
 *
 * Tile QT 0.7.x format
 * http://www.mapeditor.org/
 *
 */

(function($) {

	/**
	 * a collection of TMX utility Function
	 * @final
	 * @memberOf me
	 * @ignore
	 */

	me.TMXUtils = (function() {

		/**
		 * set and interpret a TMX property value
		 * @ignore
		 */
		function setTMXValue(value) {
			if (!value || value.isBoolean()) {
				// if value not defined or boolean
				value = value ? (value === "true") : true;
			} else if (value.isNumeric()) {
				// check if numeric
				value = Number(value);
			} else if (value.match(/^json:/i)) {
				// try to parse it
				var match = value.split(/^json:/i)[1];
				try {
					value = JSON.parse(match);
				}
				catch (e) {
					throw "Unable to parse JSON: " + match;
				}
			}
			// return the interpreted value
			return value;
		}

		// hold public stuff in our singleton
		var api = {};

		/**
		 * Apply TMX Properties to the give object
		 * @ignore
		 */
		api.applyTMXPropertiesFromXML = function(obj, xmldata) {
			var properties = xmldata.getElementsByTagName(me.TMX_TAG_PROPERTIES)[0];

			if (properties) {
				var oProp = properties.getElementsByTagName(me.TMX_TAG_PROPERTY);

				for ( var i = 0; i < oProp.length; i++) {
					var propname = me.mapReader.TMXParser.getStringAttribute(oProp[i], me.TMX_TAG_NAME);
					var value = me.mapReader.TMXParser.getStringAttribute(oProp[i], me.TMX_TAG_VALUE);
					// set the value
					obj[propname] = setTMXValue(value);

				}
			}

		};

		/**
		 * Apply TMX Properties to the give object
		 * @ignore
		 */
		api.applyTMXPropertiesFromJSON = function(obj, data) {
			var properties = data[me.TMX_TAG_PROPERTIES];
			if (properties) {
				obj.mixin(properties);
			}
		};

		// return our object
		return api;

	})();

	/*---------------------------------------------------------*/
	// END END END
	/*---------------------------------------------------------*/
})(window);
<commit_msg>[#405] fixed a regression when parsing properties in Tiled Json maps<commit_after>/*
 * MelonJS Game Engine
 * Copyright (C) 2011 - 2013, Olivier BIOT
 * http://www.melonjs.org
 *
 * Tile QT 0.7.x format
 * http://www.mapeditor.org/
 *
 */

(function($) {

	/**
	 * a collection of TMX utility Function
	 * @final
	 * @memberOf me
	 * @ignore
	 */

	me.TMXUtils = (function() {

		/**
		 * set and interpret a TMX property value
		 * @ignore
		 */
		function setTMXValue(value) {
			if (!value || value.isBoolean()) {
				// if value not defined or boolean
				value = value ? (value === "true") : true;
			} else if (value.isNumeric()) {
				// check if numeric
				value = Number(value);
			} else if (value.match(/^json:/i)) {
				// try to parse it
				var match = value.split(/^json:/i)[1];
				try {
					value = JSON.parse(match);
				}
				catch (e) {
					throw "Unable to parse JSON: " + match;
				}
			}
			// return the interpreted value
			return value;
		}

		// hold public stuff in our singleton
		var api = {};

		/**
		 * Apply TMX Properties to the give object
		 * @ignore
		 */
		api.applyTMXPropertiesFromXML = function(obj, xmldata) {
			var properties = xmldata.getElementsByTagName(me.TMX_TAG_PROPERTIES)[0];

			if (properties) {
				var oProp = properties.getElementsByTagName(me.TMX_TAG_PROPERTY);

				for ( var i = 0; i < oProp.length; i++) {
					var propname = me.mapReader.TMXParser.getStringAttribute(oProp[i], me.TMX_TAG_NAME);
					var value = me.mapReader.TMXParser.getStringAttribute(oProp[i], me.TMX_TAG_VALUE);
					// set the value
					obj[propname] = setTMXValue(value);

				}
			}

		};

		/**
		 * Apply TMX Properties to the give object
		 * @ignore
		 */
		api.applyTMXPropertiesFromJSON = function(obj, data) {
			var properties = data[me.TMX_TAG_PROPERTIES];
			if (properties) {
				for(var name in properties){
					if (properties.hasOwnProperty(name)) {
						// set the value
						obj[name] = setTMXValue(properties[name]);
					}
				}
			}
		};

		// return our object
		return api;

	})();

	/*---------------------------------------------------------*/
	// END END END
	/*---------------------------------------------------------*/
})(window);
<|endoftext|>


<commit_before>import React, { Component, PropTypes } from 'react';
import ReactDOM from 'react-dom';
import { connect } from 'react-redux';

import * as actions from './actions';

export const Outline = connect(state => {
  return { nodes: state.nodes.toJS() };
})(React.createClass({
  getInitialState() {
    return { selected: null };
  },
  render() {
    const { dispatch, nodes } = this.props;
    const selection = {
      set: (path) => this.setState({ selected: path }),
      get: () => this.state.selected,
      clear: () => this.setState({ selected: null })
    };
    return (
      <OutlineTree path="" dispatch={dispatch} nodes={nodes}
                   selection={selection} />
    );
  },
}));

export const OutlineTree = ({ dispatch, nodes, selection, path }) =>

    {nodes.map((node, index) =>
      <OutlineNode dispatch={dispatch} selection={selection}
                   node={node} key={index} index={index}
                   path={path + index} />
    )}

;

export const OutlineNode = React.createClass({
  getInitialState() {
    return {
      editing: false,
      editorValue: this.props.node.title,
      dragging: false,
      positionPreview: null
    };
  },
  render() {
    const { dispatch, node, path, selection } = this.props;
    const { positionPreview, editing, editorValue, dragging } = this.state;

    const selected = selection.get() === path;

    const style = {
      padding: '0.125em',
      backgroundColor: positionPreview == actions.MovePositions.ADOPT ?
        '#ccc' : 'transparent',
      borderTop: positionPreview == actions.MovePositions.BEFORE ?
        '1px solid #ccc' : '1px solid transparent',
      borderBottom: positionPreview == actions.MovePositions.AFTER ?
        '1px solid #ccc' : '1px solid transparent',
      opacity: dragging ? 0.5 : 1
    };
    const titleStyle = {
      fontFamily: 'sans-serif',
      fontSize: '14px',
      margin: ' 0 0.25em 0 0',
      padding: '0.25em 0.25em',
      border: selected ?
        '1px dashed #ccc' : '1px solid transparent'
    };
    const editorStyle = {
      fontFamily: 'sans-serif',
      fontSize: '14px',
      margin: ' 0 0.25em 0 0',
      padding: '0.25em 0.25em',
      border: '1px solid #ccc'
    };
    const buttonStyle = {
      fontFamily: 'monospace',
      margin: "0 0.25em"
    };

    return (

        <button style={buttonStyle}
                disabled={!node.children}
                onClick={this.onToggleCollapsed}>
          {!node.children ? 'o' : node.collapsed ? '+' : '-'}
        </button>

        <button style={buttonStyle} onClick={this.onDelete}>X</button>

        {selected && editing ?
          <input className="editor"
                 style={editorStyle}
                 autoFocus={true}
                 ref={moveCursorToEnd}
                 type="text"
                 size="50"
                 value={editorValue}
                 onBlur={this.onEditorBlur}
                 onChange={this.onEditorChange} />
          :
          {node.title}}

        {!node.collapsed && node.children &&
          <OutlineTree dispatch={dispatch} selection={selection}
                       path={path + '.children.'} nodes={node.children} />}

    );
  },
  onSelectionClick(ev) {
    this.props.selection.set(this.props.path);
  },
  onTitleDoubleClick(ev) {
    this.setState({ editing: true });
  },
  onEditorChange(ev) {
    this.setState({ editorValue: ev.target.value });
  },
  onEditorBlur(ev) {
    const { dispatch, node, path } = this.props;
    dispatch(actions.setNodeAttribute(path, 'title',
                                      this.state.editorValue));
    this.setState({ editing: false });
  },
  onDragStart(ev) {
    const { path, node } = this.props;
    setDragMeta(ev, {path});
    ev.dataTransfer.effectAllowed = 'move';
    ev.dataTransfer.setData('text/plain', JSON.stringify({path, node}));
    ev.stopPropagation();
    this.setState({ dragging: true });
  },
  onDragEnter(ev) {
    const { path: draggedPath } = getDragMeta(ev);
    if (draggedPath != this.props.path) {
      ev.dataTransfer.dropEffect = 'move';
    }
    return stahp(ev);
  },
  onDragOver(ev) {
    const { path: draggedPath } = getDragMeta(ev);
    // Ensure the drop target is not the dragged node or a child
    if (this.props.path.indexOf(draggedPath) !== 0) {
      const rect = ev.target.getBoundingClientRect();
      // TODO: Get rid of the magic number here for defining the zone width
      // that defines whether the drop will be an adoption or before/after
      const pos = (ev.clientX > (rect.left + 50)) ? 'ADOPT' :
                  (ev.clientY < (rect.top + rect.height / 2)) ? 'BEFORE' :
                  'AFTER';
      this.setState({ positionPreview: actions.MovePositions[pos] });
    }
    return stahp(ev);
  },
  onDragLeave(ev) {
    this.setState({ positionPreview: null });
    return stahp(ev);
  },
  onDragEnd(ev) {
    this.setState({ dragging: false });
    return stahp(ev);
  },
  onDrop(ev) {
    const { dispatch } = this.props;
    // TODO: Accept drops from outside the browser.
    const { path: draggedPath } = getDragMeta(ev);
    const data = JSON.parse(ev.dataTransfer.getData('text'));
    // Ensure the drop target is not the dragged node or a child
    if (this.props.path.indexOf(draggedPath) !== 0) {
      dispatch(actions.moveNode(data.path, this.props.path,
                                this.state.positionPreview));
    }
    this.setState({ positionPreview: null });
    return stahp(ev);
  },
  onDelete(ev) {
    const { dispatch } = this.props;
    dispatch(actions.deleteNode(this.props.path))
    return stahp(ev);
  },
  onToggleCollapsed(ev) {
    const { dispatch, node, path } = this.props;
    dispatch(actions.setNodeAttribute(path, 'collapsed', !node.collapsed ));
    return stahp(ev);
  }
});

function stahp(ev) {
  ev.stopPropagation();
  ev.preventDefault();
}

// HACK: Encode data in type names to circumvent dataTransfer protected mode
// http://www.w3.org/TR/2011/WD-html5-20110113/dnd.html#concept-dnd-p
function setDragMeta(ev, data) {
  Object.keys(data).forEach(key => {
    ev.dataTransfer.setData('x-meta/' + key + '/' + data[key], data[key]);
  });
}

// HACK: Decode data from type names to circumvent dataTransfer protected mode
// http://www.w3.org/TR/2011/WD-html5-20110113/dnd.html#concept-dnd-p
function getDragMeta(ev) {
  const data = {};
  const types = ev.dataTransfer.types;
  for (let i = 0; i < types.length; i++) {
    const parts = types[i].split('/');
    if (parts[0] == 'x-meta') {
      data[parts[1]] = parts[2];
    }
  }
  return data;
}

// https://davidwalsh.name/caret-end
function moveCursorToEnd(el) {
  if (!el) { return; }
  if (typeof el.selectionStart == "number") {
    el.selectionStart = el.selectionEnd = el.value.length;
  } else if (typeof el.createTextRange != "undefined") {
    el.focus();
    var range = el.createTextRange();
    range.collapse(false);
    range.select();
  }
}
<commit_msg>Disable all draggable nodes when editing node contents<commit_after>import React, { Component, PropTypes } from 'react';
import ReactDOM from 'react-dom';
import { connect } from 'react-redux';

import * as actions from './actions';

export const Outline = connect(state => {
  return { nodes: state.nodes.toJS() };
})(React.createClass({
  getInitialState() {
    return {
      selected: null,
      editing: false
    };
  },
  render() {
    const { dispatch, nodes } = this.props;
    const rootState = {
      get: (name) => this.state[name],
      set: (name, value) => {
        const data = {};
        data[name] = value;
        this.setState(data);
      }
    };
    return (
      <OutlineTree path="" dispatch={dispatch} nodes={nodes}
                   rootState={rootState} />
    );
  }
}));

export const OutlineTree = ({ dispatch, nodes, rootState, path }) =>

    {nodes.map((node, index) =>
      <OutlineNode dispatch={dispatch} rootState={rootState}
                   node={node} key={index} index={index}
                   path={path + index} />
    )}

;

export const OutlineNode = React.createClass({
  getInitialState() {
    return {
      editing: false,
      editorValue: this.props.node.title,
      dragging: false,
      positionPreview: null
    };
  },
  render() {
    const { dispatch, node, path, rootState } = this.props;
    const { positionPreview, editing, editorValue, dragging } = this.state;

    const selected = rootState.get('selection') === path;

    const style = {
      listStyleType: 'none',
      padding: '0.125em',
      backgroundColor: positionPreview == actions.MovePositions.ADOPT ?
        '#ccc' : 'transparent',
      borderTop: positionPreview == actions.MovePositions.BEFORE ?
        '1px solid #ccc' : '1px solid transparent',
      borderBottom: positionPreview == actions.MovePositions.AFTER ?
        '1px solid #ccc' : '1px solid transparent',
      opacity: dragging ? 0.5 : 1
    };
    const titleStyle = {
      fontFamily: 'sans-serif',
      fontSize: '14px',
      margin: ' 0 0.25em 0 0',
      padding: '0.25em 0.25em',
      border: selected ?
        '1px dashed #ccc' : '1px solid transparent'
    };
    const editorStyle = {
      display: 'block',
      width: '75%',
      fontFamily: 'sans-serif',
      fontSize: '14px',
      margin: ' 0 0.25em 0 0',
      padding: '0.25em 0.25em',
      border: '1px solid #ccc'
    };
    const buttonStyle = {
      fontFamily: 'monospace',
      margin: '0 0.25em'
    };

    // HACK: Disable dragging when any node is edited.
    // On Firefox, input fields don't receive mouse clicks
    // when a parent has draggable=true
    //
    // https://bugzilla.mozilla.org/show_bug.cgi?id=800050
    // https://bugzilla.mozilla.org/show_bug.cgi?id=1189486
    const draggable = !rootState.get('editing');

    return (

        <button style={buttonStyle}
                disabled={!node.children}
                onClick={this.onToggleCollapsed}>
          {!node.children ? 'o' : node.collapsed ? '+' : '-'}
        </button>

        <button style={buttonStyle} onClick={this.onDelete}>X</button>

        {selected && editing ?
          <input className="editor"
                 style={editorStyle}
                 autoFocus={true}
                 ref={moveCursorToEnd}
                 type="text"
                 size="50"
                 value={editorValue}
                 onBlur={this.onEditorBlur}
                 onChange={this.onEditorChange} />
          :
          {node.title}}

        {!node.collapsed && node.children &&
          <OutlineTree dispatch={dispatch} rootState={rootState}
                       path={path + '.children.'} nodes={node.children} />}

    );
  },
  setEditing(isEditing) {
    this.setState({ editing: isEditing });
    // HACK: Track editing on the root state, so we can disable all dragging
    this.props.rootState.set('editing', isEditing);
  },
  onSelectionClick(ev) {
    this.props.rootState.set('selection', this.props.path);
  },
  onTitleDoubleClick(ev) {
    this.setEditing(true);
  },
  onEditorChange(ev) {
    this.setState({ editorValue: ev.target.value });
  },
  onEditorBlur(ev) {
    const { dispatch, node, path } = this.props;
    if (this.state.editorValue !== this.props.node.title) {
      dispatch(actions.setNodeAttribute(
        path, 'title', this.state.editorValue));
    }
    this.setEditing(false);
  },
  onDragStart(ev) {
    const { path, node } = this.props;
    setDragMeta(ev, {path});
    ev.dataTransfer.effectAllowed = 'move';
    ev.dataTransfer.setData('text/plain', JSON.stringify({path, node}));
    ev.stopPropagation();
    this.setState({ dragging: true });
  },
  onDragEnter(ev) {
    const { path: draggedPath } = getDragMeta(ev);
    if (draggedPath != this.props.path) {
      ev.dataTransfer.dropEffect = 'move';
    }
    return stahp(ev);
  },
  onDragOver(ev) {
    const { path: draggedPath } = getDragMeta(ev);
    // Ensure the drop target is not the dragged node or a child
    if (this.props.path.indexOf(draggedPath) !== 0) {
      const rect = ev.target.getBoundingClientRect();
      // TODO: Get rid of the magic number here for defining the zone width
      // that defines whether the drop will be an adoption or before/after
      const pos = (ev.clientX > (rect.left + 50)) ? 'ADOPT' :
                  (ev.clientY < (rect.top + rect.height / 2)) ? 'BEFORE' :
                  'AFTER';
      this.setState({ positionPreview: actions.MovePositions[pos] });
    }
    return stahp(ev);
  },
  onDragLeave(ev) {
    this.setState({ positionPreview: null });
    return stahp(ev);
  },
  onDragEnd(ev) {
    this.setState({ dragging: false });
    return stahp(ev);
  },
  onDrop(ev) {
    const { dispatch } = this.props;
    // TODO: Accept drops from outside the browser.
    const { path: draggedPath } = getDragMeta(ev);
    const data = JSON.parse(ev.dataTransfer.getData('text'));
    // Ensure the drop target is not the dragged node or a child
    if (this.props.path.indexOf(draggedPath) !== 0) {
      dispatch(actions.moveNode(data.path, this.props.path,
                                this.state.positionPreview));
    }
    this.setState({ positionPreview: null });
    return stahp(ev);
  },
  onDelete(ev) {
    const { dispatch } = this.props;
    dispatch(actions.deleteNode(this.props.path))
    return stahp(ev);
  },
  onToggleCollapsed(ev) {
    const { dispatch, node, path } = this.props;
    dispatch(actions.setNodeAttribute(path, 'collapsed', !node.collapsed ));
    return stahp(ev);
  }
});

function stahp(ev) {
  ev.stopPropagation();
  ev.preventDefault();
}

// HACK: Encode data in type names to circumvent dataTransfer protected mode
// http://www.w3.org/TR/2011/WD-html5-20110113/dnd.html#concept-dnd-p
function setDragMeta(ev, data) {
  Object.keys(data).forEach(key => {
    ev.dataTransfer.setData('x-meta/' + key + '/' + data[key], data[key]);
  });
}

// HACK: Decode data from type names to circumvent dataTransfer protected mode
// http://www.w3.org/TR/2011/WD-html5-20110113/dnd.html#concept-dnd-p
function getDragMeta(ev) {
  const data = {};
  const types = ev.dataTransfer.types;
  for (let i = 0; i < types.length; i++) {
    const parts = types[i].split('/');
    if (parts[0] == 'x-meta') {
      data[parts[1]] = parts[2];
    }
  }
  return data;
}

// https://davidwalsh.name/caret-end
function moveCursorToEnd(el) {
  if (!el) { return; }
  if (typeof el.selectionStart == "number") {
    el.selectionStart = el.selectionEnd = el.value.length;
  } else if (typeof el.createTextRange != "undefined") {
    el.focus();
    var range = el.createTextRange();
    range.collapse(false);
    range.select();
  }
}
<|endoftext|>


<commit_before>import omit from 'lodash/omit';
import values from 'lodash/values';
import url from 'url';

import { ADBLOCK_TEST_ID } from 'app/constants';

import { isHidden } from 'lib/dom';
import isFakeSubreddit from 'lib/isFakeSubreddit';
import { getEventTracker } from 'lib/eventTracker';
import * as gtm from 'lib/gtm';

const ID_REGEX = /(?:t\d+_)?(.*)/;

export function removePrefix(prefixedId) {
  return ID_REGEX.exec(prefixedId)[1];
}

export function convertId(id) {
  return parseInt(removePrefix(id), 36);
}

export function buildSubredditData(state) {
  const { subredditName } = state.platform.currentPage.urlParams;

  if (!subredditName || isFakeSubreddit(subredditName)) { return {}; }

  const subreddit = state.subreddits[subredditName.toLowerCase()];
  if (!subreddit) { return {}; }

  return {
    sr_id: convertId(subreddit.name),
    sr_name: subreddit.uuid,
  };
}

export function getListingName(state) {
  const subredditName = state.platform.currentPage.urlParams.subredditName;
  return { 'listing_name': subredditName ? subredditName : 'frontpage'};
  // we don't support multis yet but we will need to update this when we do.
}

export function getUserInfoOrLoid(state) {
  const user = state.user;
  const userInfo = state.accounts[user.name];
  if (!user.loggedOut) {
    return {
      'user_id': convertId(userInfo.id),
      'user_name': user.name,
    };
  }

  const loid = state.loid;
  return {
    'loid': loid.loid,
    'loid_created': loid.loidCreated,
  };
}

function getDomain(referrer, meta) {
  const x = url.parse(referrer);
  return x.host || meta.domain;
}

export function getBasePayload(state) {
  // NOTE: this is only for usage on the client since it has references to window
  const { platform, meta, compact, preferences } = state;
  const referrer = platform.currentPage.referrer;

  const payload = {
    domain: meta.domain,
    geoip_country: meta.country,
    user_agent: meta.userAgent,
    base_url: platform.currentPage.url,
    referrer_domain: referrer ? getDomain(referrer, meta) : '',
    referrer_url: referrer,
    language: preferences.lang,
    dnt: !!window.DO_NOT_TRACK,
    compact_view: compact,
    adblock: hasAdblock(),
    ...getUserInfoOrLoid(state),
  };

  return payload;
}

function trackScreenViewEvent(state, additionalEventData) {
  const payload = {
    ...getBasePayload(state),
    ...buildSubredditData(state),
    ...additionalEventData,
  };
  getEventTracker().track('screenview_events', 'cs.screenview_mweb', payload);
}

export function trackExperimentClickEvent(state, experimentName, experimentId, targetThing) {
  const payload = {
    ...getBasePayload(state),
    'experiment_name': experimentName,
    'experiment_id': experimentId,
    'target_fullname': targetThing.name,
    'target_url': targetThing.url,
    'target_type': targetThing.type === 'post' ? targetThing.isSelf ? 'self' : 'link' : targetThing.type,
    'target_id': convertId(targetThing.id),
    'target_name': targetThing.type === 'subreddit' ? targetThing.displayName : undefined,
  };
  getEventTracker().track('internal_click_events', 'cs.experiment_click', payload);
}

function trackCrawlEvent(state, additionalEventData) {
  const { protocol, method, crawler, userAgent, domain } = state.meta;

  const payload = {
    params_app: 'mweb',
    http_response_code: state.platform.currentPage.status,
    // (skrisman | 10.17.2016) consider how we can get a response_time here
    // (skrisman | 10.17.2016) is there a concept like "server" that we have?
    crawler_name: crawler,
    method,
    protocol,
    domain,
    user_agent: userAgent,
    base_url: state.platform.currentPage.url,
    ...additionalEventData,
  };

  getEventTracker().track('crawl_events', 'url_crawl', payload);
}

const IGNORE_PARAMS = ['overlayMenu', 'commentReply'];
let lastUrlToken = null;

function isDuplicatePageView(state) {
  // NOTE: This block is a total hack to fix multiple pageviews. The way it
  // works is by normalizing urls and their parameters. If a query parameter
  // is in the ignore list, then it doesn't dirty the url and doesn't
  // contribute to a page view.
  // DELETE after ephemeral views having urls is fixed.
  const paramToken = values(omit(state.platform.currentPage.queryParams, IGNORE_PARAMS))
    .sort()
    .join('-');

  const urlToken = state.platform.currentPage.url + paramToken;
  if (urlToken !== lastUrlToken) {
    lastUrlToken = urlToken;
    return false;
  }
  return true;
}

export function trackPageEvents(state, additionalEventData={}) {
  if (isDuplicatePageView(state)) {
    return;
  }

  if (process.env.ENV === 'client') {
    gtmPageView(state);
    trackScreenViewEvent(state, additionalEventData);
  } else if (state.meta.crawler) {
    trackCrawlEvent(state, additionalEventData);
  }
}

export function trackPreferenceEvent(state, additionalEventData={}) {
  const payload = {
    ...getBasePayload(state),
    ...additionalEventData,
  };
  getEventTracker().track('user_preference_events', 'cs.save_preference_cookie', payload);
}

const gtmPageView = state => {
  const { platform: { currentPage }} = state;

  gtm.trigger('pageview', {
    subreddit: currentPage.urlParams.subredditName || '',
    pathname: currentPage.url || '/',
  });
};

const hasAdblock = () => {
  const adblockTester = document.getElementById(ADBLOCK_TEST_ID);
  // If the div has been removed, they have adblock
  if (!adblockTester) { return true; }

  const rect = adblockTester.getBoundingClientRect();
  if (!rect || !rect.height || !rect.width) {
    return true;
  }

  return isHidden(adblockTester);
};

// Tracks the active blocking of an ad
// method is how the ad was blocked
// placementIndex is the position in the listview
// state is the entire redux state
export const logClientAdblock = (method, placementIndex, state) => {
  if (process.env.ENV !== 'client') { return; }

  const payload = {
    ...getBasePayload(state),
    ...buildSubredditData(state),
    method,
    placement_type: 'native',
    placement_index: placementIndex,
    in_feed: placementIndex !== 0,
  };

  getEventTracker().track('ad_serving_events', 'cs.adblock', payload);
};
<commit_msg>Revert "Fix referrer domain"<commit_after>import omit from 'lodash/omit';
import values from 'lodash/values';
import url from 'url';

import { ADBLOCK_TEST_ID } from 'app/constants';

import { isHidden } from 'lib/dom';
import isFakeSubreddit from 'lib/isFakeSubreddit';
import { getEventTracker } from 'lib/eventTracker';
import * as gtm from 'lib/gtm';

const ID_REGEX = /(?:t\d+_)?(.*)/;

export function removePrefix(prefixedId) {
  return ID_REGEX.exec(prefixedId)[1];
}

export function convertId(id) {
  return parseInt(removePrefix(id), 36);
}

export function buildSubredditData(state) {
  const { subredditName } = state.platform.currentPage.urlParams;

  if (!subredditName || isFakeSubreddit(subredditName)) { return {}; }

  const subreddit = state.subreddits[subredditName.toLowerCase()];
  if (!subreddit) { return {}; }

  return {
    sr_id: convertId(subreddit.name),
    sr_name: subreddit.uuid,
  };
}

export function getListingName(state) {
  const subredditName = state.platform.currentPage.urlParams.subredditName;
  return { 'listing_name': subredditName ? subredditName : 'frontpage'};
  // we don't support multis yet but we will need to update this when we do.
}

export function getUserInfoOrLoid(state) {
  const user = state.user;
  const userInfo = state.accounts[user.name];
  if (!user.loggedOut) {
    return {
      'user_id': convertId(userInfo.id),
      'user_name': user.name,
    };
  }

  const loid = state.loid;
  return {
    'loid': loid.loid,
    'loid_created': loid.loidCreated,
  };
}

export function getBasePayload(state) {
  // NOTE: this is only for usage on the client since it has references to window
  const referrer = state.platform.currentPage.referrer || '';

  const payload = {
    domain: state.meta.domain,
    geoip_country: state.meta.country,
    user_agent: state.meta.userAgent,
    base_url: state.platform.currentPage.url,
    referrer_domain: url.parse(referrer).host || state.meta.domain,
    referrer_url: referrer,
    language: state.preferences.lang,
    dnt: !!window.DO_NOT_TRACK,
    compact_view: state.compact,
    adblock: hasAdblock(),
    ...getUserInfoOrLoid(state),
  };

  return payload;
}

function trackScreenViewEvent(state, additionalEventData) {
  const payload = {
    ...getBasePayload(state),
    ...buildSubredditData(state),
    ...additionalEventData,
  };
  getEventTracker().track('screenview_events', 'cs.screenview_mweb', payload);
}

export function trackExperimentClickEvent(state, experimentName, experimentId, targetThing) {
  const payload = {
    ...getBasePayload(state),
    'experiment_name': experimentName,
    'experiment_id': experimentId,
    'target_fullname': targetThing.name,
    'target_url': targetThing.url,
    'target_type': targetThing.type === 'post' ? targetThing.isSelf ? 'self' : 'link' : targetThing.type,
    'target_id': convertId(targetThing.id),
    'target_name': targetThing.type === 'subreddit' ? targetThing.displayName : undefined,
  };
  getEventTracker().track('internal_click_events', 'cs.experiment_click', payload);
}

function trackCrawlEvent(state, additionalEventData) {
  const { protocol, method, crawler, userAgent, domain } = state.meta;

  const payload = {
    params_app: 'mweb',
    http_response_code: state.platform.currentPage.status,
    // (skrisman | 10.17.2016) consider how we can get a response_time here
    // (skrisman | 10.17.2016) is there a concept like "server" that we have?
    crawler_name: crawler,
    method,
    protocol,
    domain,
    user_agent: userAgent,
    base_url: state.platform.currentPage.url,
    ...additionalEventData,
  };

  getEventTracker().track('crawl_events', 'url_crawl', payload);
}

const IGNORE_PARAMS = ['overlayMenu', 'commentReply'];
let lastUrlToken = null;

function isDuplicatePageView(state) {
  // NOTE: This block is a total hack to fix multiple pageviews. The way it
  // works is by normalizing urls and their parameters. If a query parameter
  // is in the ignore list, then it doesn't dirty the url and doesn't
  // contribute to a page view.
  // DELETE after ephemeral views having urls is fixed.
  const paramToken = values(omit(state.platform.currentPage.queryParams, IGNORE_PARAMS))
    .sort()
    .join('-');

  const urlToken = state.platform.currentPage.url + paramToken;
  if (urlToken !== lastUrlToken) {
    lastUrlToken = urlToken;
    return false;
  }
  return true;
}

export function trackPageEvents(state, additionalEventData={}) {
  if (isDuplicatePageView(state)) {
    return;
  }

  if (process.env.ENV === 'client') {
    gtmPageView(state);
    trackScreenViewEvent(state, additionalEventData);
  } else if (state.meta.crawler) {
    trackCrawlEvent(state, additionalEventData);
  }
}

export function trackPreferenceEvent(state, additionalEventData={}) {
  const payload = {
    ...getBasePayload(state),
    ...additionalEventData,
  };
  getEventTracker().track('user_preference_events', 'cs.save_preference_cookie', payload);
}

const gtmPageView = state => {
  const { platform: { currentPage }} = state;

  gtm.trigger('pageview', {
    subreddit: currentPage.urlParams.subredditName || '',
    pathname: currentPage.url || '/',
  });
};

const hasAdblock = () => {
  const adblockTester = document.getElementById(ADBLOCK_TEST_ID);
  // If the div has been removed, they have adblock
  if (!adblockTester) { return true; }

  const rect = adblockTester.getBoundingClientRect();
  if (!rect || !rect.height || !rect.width) {
    return true;
  }

  return isHidden(adblockTester);
};

// Tracks the active blocking of an ad
// method is how the ad was blocked
// placementIndex is the position in the listview
// state is the entire redux state
export const logClientAdblock = (method, placementIndex, state) => {
  if (process.env.ENV !== 'client') { return; }

  const payload = {
    ...getBasePayload(state),
    ...buildSubredditData(state),
    method,
    placement_type: 'native',
    placement_index: placementIndex,
    in_feed: placementIndex !== 0,
  };

  getEventTracker().track('ad_serving_events', 'cs.adblock', payload);
};
<|endoftext|>


<commit_before>
/**
 * Test performancce of array concat and unshift.
 *
 * @see {@link https://www.measurethat.net/Benchmarks/Show/9225/0/colorfulcakechen-array-concat-unshift-f38d5e6c5a6a0096f}
 */

/**
 * A test set.
 */
class TestSet {

  /**
   */
  constructor( arrayListLength ) {

    this.arrayListLength = arrayListLength;
    this.arrayList = new Array( arrayListLength );

    for ( let i = 0; i < this.arrayListLength; ++i ) {
      this.arrayList[ i ] = new Array( 1 );
      this.arrayList[ i ][ 0 ] = i;
    }
  }

  test_ArrayConcat() {
    let result = [];
    for ( let i = 0; i < this.arrayListLength; ++i ) {
      result = result.concat( this.arrayList[ i ] );
    }
  }

  test_ArrayUnshift() {
    let result = [];
    for ( let i = ( this.arrayListLength - 1 ); i >= 0; --i ) {
      result.unshift( ...this.arrayList[ i ] );
    }
  }

  // Testing whether the results of different implementation are the same. Also, pre-compile the codes.
  async testCaseLoader() {
    this.test_ArrayConcat();
    this.test_ArrayUnshift();
  }

}

globalThis.testSet = new TestSet( 100 );
<commit_msg>Update jsPerf_ArrayConcatUnshift.js<commit_after>
/**
 * Test performancce of array concat and unshift.
 *
 * @see {@link https://www.measurethat.net/Benchmarks/Show/9225/1/colorfulcakechen-array-concat-unshift-d01e94e8696ce742e}
 */

/**
 * A test set.
 */
class TestSet {

  /**
   */
  constructor( arrayListLength ) {

    this.arrayListLength = arrayListLength;
    this.arrayList = new Array( arrayListLength );

    for ( let i = 0; i < this.arrayListLength; ++i ) {
      this.arrayList[ i ] = new Array( 1 );
      this.arrayList[ i ][ 0 ] = i;
    }
  }

  test_ArrayConcat() {
    let result = [];
    for ( let i = 0; i < this.arrayListLength; ++i ) {
      result = result.concat( this.arrayList[ i ] );
    }
  }

  test_ArrayUnshift() {
    let result = [];
    for ( let i = ( this.arrayListLength - 1 ); i >= 0; --i ) {
      result.unshift( ...this.arrayList[ i ] );
    }
  }

  // Testing whether the results of different implementation are the same. Also, pre-compile the codes.
  async testCaseLoader() {
    this.test_ArrayConcat();
    this.test_ArrayUnshift();
  }

}

globalThis.testSet = new TestSet( 100 );
<|endoftext|>


<commit_before>/* Copyright (C) Relevance Lab Private Limited- All Rights Reserved
 * Unauthorized copying of this file, via any medium is strictly prohibited
 * Proprietary and confidential
 * Written by Relevance UI Team,
 * Aug 2015
 */

(function (angular) {
	"use strict";
	angular.module('workzone.instance')
		.controller('controlPanelCtrl', ['$scope', '$rootScope', 'instance', 'workzoneServices', 'workzoneEnvironment', '$modalInstance', 'instanceLogs', function ($scope, $rootScope, instance, workzoneServices, workzoneEnvironment, $modalInstance, instanceLogs) {
			$scope.cancel = function () {
				instanceLogs.stopLogsPolling();
				$modalInstance.dismiss('cancel');
			};
			$scope.tileView = true;
			$scope.selectedTab = 'Information';
			var _tab = {
				//To activate the Information tab on Control Panel Page Load
				tab : 'Information',
				setTab : function (tabId) {
					$scope.selectedTab = tabId;
					_tab.tab = tabId;
					if(tabId==='Logs') {
						instanceLogs.scrollBottom();
					}
				},
				isSet : function (tabId) {
					return _tab.tab === tabId;
				},
				templates: {
					actionhistory: {
						"url": "src/partials/sections/dashboard/workzone/instance/manage/templates/cpActionHistory.html"
					},
					Schedule: {
						"url": "src/partials/sections/dashboard/workzone/instance/manage/templates/cpSchedule.html"
					},
					information: {
						"url": "src/partials/sections/dashboard/workzone/instance/manage/templates/cpInfo.html"
					},
					services: {
						"url": "src/partials/sections/dashboard/workzone/instance/manage/templates/cpServices.html"
					},
					logs: {
						"url": "src/partials/sections/dashboard/workzone/instance/manage/templates/cpLogs.html"
					},
					instanceactions: {
						"url": "src/partials/sections/dashboard/workzone/instance/manage/templates/cpActions.html"
					}
				}
			};
			$scope.tab = _tab;
			if (instance.puppet) {
				//Dont show Services tab for puppet instance
				$scope.showServicesTab = false;
			} else {
				$scope.showServicesTab = true;
			}
			//The cpInstance from this scope is used in the controllers of child tabs.
			$scope.cpInstance = instance;
			console.log(instance);
			$scope.instInfo = $scope.cpInstance;
			//To activate the selected tab
			$scope.activateTab = function (tabName) {
				$scope.tab.setTab(tabName);
			};
		}
	]).controller('cpScheduleCtrl', ['$scope','genericServices','toastr','$timeout', function ($scope, genericServices,toastr,$timeout) {
		var cpInstance = $scope.$parent.cpInstance;
		$scope.schedule={
			instanceIds:[cpInstance._id],
			schedulerStartOn:moment(new Date()).format('MM/DD/YYYY'),
			schedulerEndOn:moment(new Date()).format('MM/DD/YYYY'),
			interval:[{ind:0,"days":[],"action":"start"}]
		};
		$timeout(function(){$('input.time').trigger('click');},100);
		var params={
			url:'/instances/'+cpInstance._id
			//url:'src/partials/sections/dashboard/workzone/data/oneIns.json'
		}
		genericServices.promiseGet(params).then(function (result) {
			$scope.schedule={
				isScheduled:result.isScheduled,
				schedulerStartOn:moment(result.schedulerStartOn).format('MM/DD/YYYY'),
				schedulerEndOn:moment(result.schedulerEndOn).format('MM/DD/YYYY'),
				interval:result.interval
			};
		});
		$scope.addNewTime = function () {
			$scope.schedule.interval.push({ind: $scope.schedule.length,"days":[],"action":"start"});
			$timeout(function(){$('input.time').trigger('click');},100);
		};
		$scope.selectDays=function (d,i) {
			if($scope.schedule.interval[i].days.indexOf(d) === -1){
				$scope.schedule.interval[i].days.push(d);
			} else {
				$scope.schedule.interval[i].days.splice($scope.schedule.interval[i].days.indexOf(d),1);
			}

		};
		$scope.removeTime = function (ind) {
			$scope.schedule.interval.splice(ind,1);
		};
		$scope.saveOk=function () {
			var params={
				url:'/instances/schedule',
				data:$scope.schedule
			};
			genericServices.promisePut(params).then(function(){
				toastr.success('successfully created');
			});

		};
	}]);
})(angular);<commit_msg>edit section for scheduler<commit_after>/* Copyright (C) Relevance Lab Private Limited- All Rights Reserved
 * Unauthorized copying of this file, via any medium is strictly prohibited
 * Proprietary and confidential
 * Written by Relevance UI Team,
 * Aug 2015
 */

(function (angular) {
	"use strict";
	angular.module('workzone.instance')
		.controller('controlPanelCtrl', ['$scope', '$rootScope', 'instance', 'workzoneServices', 'workzoneEnvironment', '$modalInstance', 'instanceLogs', function ($scope, $rootScope, instance, workzoneServices, workzoneEnvironment, $modalInstance, instanceLogs) {
			$scope.cancel = function () {
				instanceLogs.stopLogsPolling();
				$modalInstance.dismiss('cancel');
			};
			$scope.tileView = true;
			$scope.selectedTab = 'Information';
			var _tab = {
				//To activate the Information tab on Control Panel Page Load
				tab : 'Information',
				setTab : function (tabId) {
					$scope.selectedTab = tabId;
					_tab.tab = tabId;
					if(tabId==='Logs') {
						instanceLogs.scrollBottom();
					}
				},
				isSet : function (tabId) {
					return _tab.tab === tabId;
				},
				templates: {
					actionhistory: {
						"url": "src/partials/sections/dashboard/workzone/instance/manage/templates/cpActionHistory.html"
					},
					Schedule: {
						"url": "src/partials/sections/dashboard/workzone/instance/manage/templates/cpSchedule.html"
					},
					information: {
						"url": "src/partials/sections/dashboard/workzone/instance/manage/templates/cpInfo.html"
					},
					services: {
						"url": "src/partials/sections/dashboard/workzone/instance/manage/templates/cpServices.html"
					},
					logs: {
						"url": "src/partials/sections/dashboard/workzone/instance/manage/templates/cpLogs.html"
					},
					instanceactions: {
						"url": "src/partials/sections/dashboard/workzone/instance/manage/templates/cpActions.html"
					}
				}
			};
			$scope.tab = _tab;
			if (instance.puppet) {
				//Dont show Services tab for puppet instance
				$scope.showServicesTab = false;
			} else {
				$scope.showServicesTab = true;
			}
			//The cpInstance from this scope is used in the controllers of child tabs.
			$scope.cpInstance = instance;
			console.log(instance);
			$scope.instInfo = $scope.cpInstance;
			//To activate the selected tab
			$scope.activateTab = function (tabName) {
				$scope.tab.setTab(tabName);
			};
		}
	]).controller('cpScheduleCtrl', ['$scope','genericServices','toastr','$timeout', function ($scope, genericServices,toastr,$timeout) {
		var cpInstance = $scope.$parent.cpInstance;
		$scope.schedule={
			instanceIds:[cpInstance._id],
			schedulerStartOn:moment(new Date()).format('MM/DD/YYYY'),
			schedulerEndOn:moment(new Date()).format('MM/DD/YYYY'),
			interval:[{ind:0,"days":[],"action":"start"}]
		};
		$timeout(function(){$('input.time').trigger('click');},100);
		var params={
			url:'/instances/'+cpInstance._id
			//url:'src/partials/sections/dashboard/workzone/data/oneIns.json'
		}
		genericServices.promiseGet(params).then(function (result) {
			$scope.schedule={
				instanceIds:[cpInstance._id],
				isScheduled:result.isScheduled,
				schedulerStartOn:moment(result.schedulerStartOn).format('MM/DD/YYYY'),
				schedulerEndOn:moment(result.schedulerEndOn).format('MM/DD/YYYY'),
				interval:result.interval
			};
		});
		$scope.addNewTime = function () {
			$scope.schedule.interval.push({ind: $scope.schedule.length,"days":[],"action":"start"});
			$timeout(function(){$('input.time').trigger('click');},100);
		};
		$scope.selectDays=function (d,i) {
			if($scope.schedule.interval[i].days.indexOf(d) === -1){
				$scope.schedule.interval[i].days.push(d);
			} else {
				$scope.schedule.interval[i].days.splice($scope.schedule.interval[i].days.indexOf(d),1);
			}

		};
		$scope.removeTime = function (ind) {
			$scope.schedule.interval.splice(ind,1);
		};
		$scope.saveOk=function () {
			var params={
				url:'/instances/schedule',
				data:$scope.schedule
			};
			genericServices.promisePut(params).then(function(){
				toastr.success('successfully created');
			});

		};
	}]);
})(angular);<|endoftext|>