// (c) Copyright 2006-2007.  Adobe Systems, Incorporated.  All rights reserved.

//
// Photomerge in ExtendScript.
// Translated from the original C++ automation & filter plugins
//
// John Peterson, Adobe Systems, 2006
//
// Adobe Patent or Adobe Patent Pending Invention Included Within this File


/*
@@@BUILDINFO@@@ Photomerge.jsx 3.0.0.1
*/

/*
// BEGIN__HARVEST_EXCEPTION_ZSTRING

<javascriptresource>
<name>$$$/JavaScripts/Photomerge/Menu=Photomerge...</name>
<about>$$$/JavaScripts/Photomerge/About=Photomerge^r^rCopyright 2006 Adobe Systems Incorporated. All rights reserved.^r^rCombines several photographs into one continuous image.</about>
<menu>automate</menu>
</javascriptresource>

// END__HARVEST_EXCEPTION_ZSTRING
*/

// on localized builds we pull the $$$/Strings from a .dat file
$.localize = true;

// Put header files in a "Stack Scripts Only" folder.  The "...Only" tells
// PS not to place it in the menu.  For that reason, we do -not- localize that
// portion of the folder name.
var g_StackScriptFolderPath = app.path + "/"+ localize("$$$/ScriptingSupport/InstalledScripts=Presets/Scripts") + "/"
										+ localize("$$$/Private/Exposuremerge/StackScriptOnly=Stack Scripts Only/");

$.evalFile(g_StackScriptFolderPath + "LatteUI.jsx");

$.evalFile(g_StackScriptFolderPath + "StackSupport.jsx");

$.evalFile(g_StackScriptFolderPath + "CreateImageStack.jsx");

$.evalFile(g_StackScriptFolderPath + "Geometry.jsx");

$.evalFile(g_StackScriptFolderPath + "PolyClip.jsx");

// debug level: 0-2 (0:disable, 1:break on error, 2:break at beginning)
// Must leave at zero, otherwise trapping gFileFromBridge fails on QA's debug builds.
$.level = 0; // (Window.version.search("d") != -1) ? 1 : 0;
// debugger; // launch debugger on next line

if (typeof(PMDebug) == 'undefined')
	var PMDebug = false;

/************************************************************/
// photomerge routines

// Debug - write the trapezoid in Matlab format
function dumpTrap( name, corners )
{
	var i;
	if (! PMDebug) return;
	$.write( name + "= [" );
	for (i in corners)
		$.write( ((i > 0) ? "; " : "" ) + corners[i].fX + " " + corners[i].fY );
	$.writeln( "];");
}

StackElement.prototype.dumpMLCorners = function()
{
	// Weed out file suffix (chokes matlab)
	dumpTrap( this.fName.match(/([^.]+)/)[1], this.fCorners );
}

// Set the fCorners of the layer to the bounds of the Photoshop layer.
StackElement.prototype.setCornersToLayerBounds = function( stackDoc )
{
	if (typeof(stackDoc) == "undefined")
		stackDoc = app.activeDocument;
		
	var bounds = stackDoc.layers[this.fName].bounds;
	this.fCorners = new Array();
	this.fCorners[0] = new TPoint( bounds[0].as("px"), bounds[1].as("px") );
	this.fCorners[2] = new TPoint( bounds[2].as("px"), bounds[3].as("px") );
	
	this.fCorners[1] = new TPoint( this.fCorners[2].fX, this.fCorners[0].fY );
	this.fCorners[3] = new TPoint( this.fCorners[0].fX, this.fCorners[2].fY );
}
	

// Add the corner data to the string of per-stackElement information
// that gets passed to the filter plugin
StackElement.prototype.addPieceData = function()
{
	if (typeof(this.fCorners) != "undefined")
	{
		// Add corners in place of trailing '\n'
		this.fString = this.fString.slice(0,-1) + "fCorners=";
		for (j = 0; j < 4; j++)
			this.fString += " " + this.fCorners[j].fX.toString() + " " + this.fCorners[j].fY.toString();
		this.fString += "\t";
		
		if (typeof(this.fScale) != "undefined")
			this.fString += ("fScale=" + this.fScale.toString() + "\t");
			
		if ((typeof(this.fConnectedTo) != "undefined") && this.fConnectedTo)
			this.fString += "fConnectedTo=" + this.fConnectedTo.fLayerID + "\t";
			
		if (typeof(this.fLayerID) != "undefined")
			this.fString += "fLayerID=" + this.fLayerID.toString() + "\t";
			
		this.fString += "\n";
	}
	else
		debugger;	// Corner data missing!
}

StackElement.prototype.overlapArea = function( other )
{
	if (other == this)
		return TPoint.polygonArea( this.fCorners );

	var overlapBounds = TRect.intersection( this.fBoundsCache, other.fBoundsCache );
	if (overlapBounds.isEmpty())
		return 0.0;

	var clipPoly = TPoint.intersectConvexPolygons( this.fCorners, other.fCorners );
	if (! clipPoly)
		return 0.0;
	else return TPoint.polygonArea( clipPoly );
}

// Find the points where the two quadrilaterals intersect (yes, eight is a theoretical max)
// Note situations where one piece intersects the same edge twice - special
// case handled by SoftEdgeBlend
StackElement.prototype.findQuadIntersections = function( other, intersections )
{
	var i, j;
	var curIntersections, numIntersections = 0;

	var innerEdgeIntersections = [ 0, 0, 0, 0 ];
	var outerEdgeIntersections = [ 0, 0, 0, 0 ];
	var thisEdge = false;
	var otherEdge = false;

	for (i = 0; i < 4; i++)
	{
		var eb0 = other.fCorners[i];
		var eb1 = other.fCorners[(i + 1) > 3 ? 0 : i + 1];
		curIntersections = numIntersections;
		for (j = 0; j < 4; j++)
		{
			var ed0 = this.fCorners[j];
			var ed1 = this.fCorners[(j + 1) > 3 ? 0 : j + 1];
			var cross = TPoint.lineSegmentIntersect( eb0, eb1, ed0, ed1 );
			
			if (cross != TPoint.kInfinite)
			{
				intersections[numIntersections++] = cross;
				innerEdgeIntersections[j]++;
			}
		}
		outerEdgeIntersections[i] = numIntersections - curIntersections;
	}

	if (numIntersections == 2)
		for (i = 0; i < 4; i++)
		{
			if (innerEdgeIntersections[i] == 2) thisEdge = true;
			if (outerEdgeIntersections[i] == 2) otherEdge = true;
		}
	
	return {"numIntersections":numIntersections, "thisEdge":thisEdge, "otherEdge":otherEdge};
}

// Look for a point of this that's inside the corners of other.

StackElement.prototype.findSingleInsidePoint = function( other )
{
	var i;
	var result = new Object();
	result.numFound = 0;
	for (i = 0; i < 4; i++)
		if (this.fCorners[i].pointInQuad( other.fCorners ))
		{
			result.numFound++;
			result.insidePt = this.fCorners[i];
		}
	return result;
}

// "Adobe patent application tracking # B349, entitled 'Method and apparatus for Layer-based Panorama Adjustment and Editing', inventor: John Peterson"
//
// Because we're dealing with quads, we can't use the simple scheme
// to generate blend rects that the rectangles use - black stuff
// from the rects will seep in. So instead, we use various heuristics to
// figure out how the quads themselves intersect.  This isn't too hard
// when there's just two intersection points (usual case), but there are
// some pathological cases where there are many more intersections (the octogon
// from one square 45 deg. off from another is the classic example).			

StackElement.prototype.softEdgeBlend = function( other, blendRad )
{
	var i, j;
	var intersections = new Array();	// Worst case is a square inside a 45 degree rot square

	// If there's no distortion, do the blend strictly on the rectangles
/*	if ((!IsQuadMapped() && !other->IsQuadMapped())
		|| (IsRectilinear() && other->IsRectilinear()))
	{
		Assert_( other->fWarpedRaster );
		fWarpedRaster->SoftEdgeBlendRasters( *(other->fWarpedRaster), blendRad );
		return;
	}
*/
	var bounds = this.getBounds();

	// Find the points where the two quadrilaterals intersect
	var intResult = this.findQuadIntersections( other, intersections );
	var numIntersections = intResult.numIntersections;
	var thisEdgeTwice = intResult.thisEdge;
	var otherEdgeTwice = intResult.otherEdge;
	
	var thisInsidePt = this.findSingleInsidePoint( other );
	var otherInsidePt = other.findSingleInsidePoint( this );

	// If quads don't overlap, just bail
	if (numIntersections == 0)
		return;

	// Handle cases where just one point overlaps the other piece
	
	var insidePoint;
	if (otherEdgeTwice || ((numIntersections == 2) && (thisInsidePt.numFound == 1)))
	{
		insidePoint = thisInsidePt.insidePt;
		this.makeBlendTrapezoid( intersections[0], intersections[1], insidePoint, blendRad, false );
		return;
	}
	if (thisEdgeTwice /*|| ((numIntersections == 2) && (otherInsidePt.numFound == 1))*/)
	{
		insidePoint = otherInsidePt.insidePt;
		this.makeBlendTrapezoid( intersections[0], intersections[1], insidePoint, blendRad, false );
		return;
	}

	// ...More than two corners overlap, apply heuristics to find reasonable blending 
	$.bp( numIntersections > 8 );

	// If we got more than two points, pick the two furthest apart
	if (numIntersections > 2)
	{
		var max0, max1;
		var maxDist = -1.0;

		for (i = 0; i < numIntersections - 1; i++)
			for (j = i + 1; j < numIntersections; j++)
			{
				var dist = (intersections[i] - intersections[j]).vectorLength();
				if (dist > maxDist)
				{
					max0 = intersections[i];
					max1 = intersections[j];
					maxDist = dist;
				}
			}

		$.bp( maxDist <= -1.0 );
		intersections[0] = max0;
		intersections[1] = max1;
		numIntersections = 2;
	}

	TPoint.clipLineToRect( bounds, intersections[0], intersections[1] );

	// The point furthest away from the cut line on the "other" image
	// is the one that we blend toward (the "dark" corner).
	var maxPoint = 0, maxDist = -1;
	for (i = 0; i < 4; i++)
	{
		var dist = Math.abs( other.fCorners[i].distanceToLine( intersections[0], intersections[1] ) );
		if (dist > maxDist)
		{
			maxDist = dist;
			maxPoint = i;
		}
	}

	this.makeBlendTrapezoid( intersections[0], intersections[1], other.fCorners[maxPoint], blendRad, true );
}

// "Adobe patent application tracking # B349, entitled 'Method and apparatus for Layer-based Panorama Adjustment and Editing', inventor: John Peterson"
//
// Create a layer mask that fades out from the edge0,edge1 cutline towards "insidePt"
// If two points are inside, construct the mask fade from the edge of the image.
//
//                       insidePt
//             |            *           |
//             |           / \          |
//             |          /   \         |
// insetLine-> |       +-/-----\-+      |  --
//             |       |/       \|      |   | radius
// baseLine->  +-------*---------*------+  --
//                    /edge0      \edge1
//                   /             \           
//
//
StackElement.prototype.makeBlendTrapezoid = function( edge0, edge1, insidePt, radius, useCorners )
{
	function sgn(x) { if (x < 0) return -1; if (x > 0) return 1; return 0; }
	function wrap4(i, next)
	{
		i += next;
		if (i > 3) return i % 4;
		if (i < 0) return 3;
		return i;
	}
	
	// Create a layer mask
	selectOneLayer( app.activeDocument, this.fName );
//	app.activeDocument.activeLayer = app.activeDocument.layers[this.fName];	// Broken if multiple layers selected.
	createLayerMask();	// Does nothing if the layer already has a mask
	
	// Create vector perpendicular to edge towards insidePt
	var edgeDir = edge1 - edge0;
	var blendDir = new TPoint( -edgeDir.fY, edgeDir.fX );
	blendDir /= blendDir.vectorLength();	// Make unit length
	
	// Make a polygonal selection covering the area
	var dist = insidePt.distanceToLine( edge0, edge1 );
	var blendOffset = blendDir * dist;
	var blendBox;
	if (! useCorners)
		blendBox = [edge0, edge0+blendOffset, edge1+blendOffset, edge1, edge0];
	else
	{
		// If the cutline slices across the image (two points on each side), then
		//		- Look for the edge edge0 is on
		//		- Figure out which corner is on the same side as "insdePt"
		//		- Construct the blendBox from that.
		var i, nextPtInd;
		for (i = 0; i < 4; i++)
			if (edge0.distanceToLine( this.fCorners[i], this.fCorners[wrap4(i,1)] ) < 0.0001 )
			{
				if (this.fCorners[i].sideOf( edge0, edge1 ) == sgn( dist ))
					nextPtInd = -1;
				else
					nextPtInd = 1;
				blendBox = new Array();
				blendBox[0] = edge0;
				blendBox[1] = (nextPtInd < 0) ? this.fCorners[i] : this.fCorners[i+1];
				blendBox[2] = (nextPtInd < 0) ? this.fCorners[wrap4(i, -1)] : this.fCorners[wrap4(i, 2)];
				blendBox[3] = edge1;
				break;
			}
		$.bp( i == 4 );	// Never found edge0?
	}
			
	createPolygonSelection( blendBox );
	
	// Fill it.
	var midPoint = (edge0 + edge1) * 0.5;
	gradientFillLayerMask( midPoint, midPoint + blendDir * radius * sgn(dist) );
	app.activeDocument.selection.deselect();
}

//////////////////////////////////////////////////////////////////////////////////
// Photomerge base class
//////////////////////////////////////////////////////////////////////////////////

const kPhotomergeAdvancedBlendingFlag = app.stringIDToTypeID( "PhotomergeAdvancedBlendingFlag" );	

photomerge = new ImageStackCreator( localize("$$$/AdobePlugin/Shared/Photomerge/Process/Name=Photomerge"),
										  localize('$$$/AdobePlugin/Shared/Photomerge/Auto/untitled=Untitled_Panorama' ),
										  localize('$$$/AdobePlugin/Shared/Photomerge/DlogIntro=Choose two or more files to merge into a panorama') );

// For now, alignment is turned OFF, because we want to
// invoke it independantly.			  
photomerge.useAlignment			= false;	// We do the alignment, not PS
photomerge.hideAlignment		= true;

photomerge.mustBeSameSize		= false;
photomerge.mustBeUnmodifiedRaw = false;
photomerge.mustNotBe32Bit		= false;
photomerge.radioButtons = ["_LOauto", "_LOnormal", "_LOperspective", "_LOcylindrical", "_LOinteractive"];
photomerge.interactiveFlag		= false;
photomerge.alignmentKey			= "Auto";	// Defaults to perspective
photomerge.compositionFile		= null;

photomerge.advancedBlending		= true;

try {
// We want to steer people to the advanced blending option,
// so have it be on by default, rather than sticky.
//	var desc = app.getCustomOptions("PhotomergeFlags001");
//	photomerge.advancedBlending = desc.getBoolean( kPhotomergeAdvancedBlendingFlag );
}
catch (e)
{
}


// Get the bounds of all of the stackElements.
photomerge.getBounds = function()
{
	var i;
	for (i in this.stackElements)
	{
		if (i == 0) 
			this.fBounds = this.stackElements[i].getBounds();
		else
			this.fBounds.extendTo( this.stackElements[i].getBounds() );
	}
	return this.fBounds;
}

// Align selected layers by content (uses SIFT registration in Photoshop core)
// This just returns the alignment data, it does not actually transform the layers
// unless doTransform is true
photomerge.getAlignmentInfo = function( stackDoc, doTransform )
{
	selectAllLayers(stackDoc, 1);
	const kMargin = 10;
	
	function offsetGroup( delta, group )
	{
		group.bounds.offset( delta );
		var k;
		for (k = 0; k < group.layers.length; ++k)
		{
			group.layers[k].offset( delta );
			if (doTransform)
			{
				selectOneLayer( stackDoc, group.layers[k].fName );
				// Translate gets broken when document DPI isn't 72 DPI...(PR 1417264)
//				activeDocument.activeLayer.translate( UnitValue( delta.fX, "px" ), UnitValue( delta.fY, "px" ) );	
				translateActiveLayer( delta.fX, delta.fY );
			}
		}
	}

	var i, j, alignInfo;
	alignInfo = getActiveDocAlignmentInfo( this.alignmentKey, doTransform );
	// If the alignment fails completely, fake up a plan B...
	// For now, just set the images side by side. 
	if (! alignInfo)
	{
		alert(localize("$$$/AdobePlugin/Shared/Photomerge/alignbad=Some images could not be automatically aligned"));
		var xpos = 0;
		for (i in this.stackElements)
		{
			this.stackElements[i].setCornersToSize();
			this.stackElements[i].offset( new TPoint( xpos, 0 ) );
			xpos += this.stackElements[i].getBounds().getWidth() + kMargin;
		}
		this.fGroups = null;
	}
	else
	{
		var layerList = alignInfo.layerInfo;
		this.fGroups = new Array();
		for (i = 0; i < layerList.length; ++i)
		{
			// Note we depend on stackElement's order matching 
			// the document's sheet list!
			var curGroup = layerList[i].groupNum;
			if (doTransform && layerList[i].corners.length == 0)
				this.stackElements[i].setCornersToLayerBounds( stackDoc );
			else
				this.stackElements[i].fCorners = layerList[i].corners;
			this.stackElements[i].fAlignGroup = curGroup;
			this.stackElements[i].fBaseFlag = layerList[i].baseFlag;
			if (typeof(this.fGroups[curGroup]) == "undefined")
			{
				this.fGroups[curGroup] = new Object();
				this.fGroups[curGroup].hasCorners = layerList[i].corners.length > 0;
				this.fGroups[curGroup].bounds = this.stackElements[i].getBounds();
				this.fGroups[curGroup].layers = new Array();
				this.fGroups[curGroup].xformType = layerList[i].xformType;
			}
			else
				this.fGroups[curGroup].bounds.extendTo( this.stackElements[i].getBounds() );
				
			this.fGroups[curGroup].layers.push( this.stackElements[i] );
		}
		
		// Now move the groups into  place
		// Note carefully: if the corners were given, then the group is already
		// transformed into the proper spot, and we just need to move the corners to
		// match.  So shut off moving the layer pixels around from here on out.
		if (this.fGroups[0].hasCorners)
			doTransform = false;
		offsetGroup( -this.fGroups[0].bounds.getTopLeft(), this.fGroups[0] );
		
		for (i = 1; i < alignInfo.numGroups; ++i)
		{
			var spacing = Math.round(this.fGroups[i-1].bounds.getHeight() / 10.0);
			offsetGroup( -this.fGroups[i].bounds.getTopLeft() + new TPoint(0, spacing + Math.round(this.fGroups[i-1].bounds.fBottom)), 
						   this.fGroups[i] );
		}
	}
	this.getBounds();
}

// The original Photomerge plugin needs to have the "connectivity" of the 
// pieces when in perspective mode, i.e., a pieces distortion is based
// on the distortion of the one it overlaps most.  This takes the 
// "base" piece information from the PS core and uses overlap area to
// determine this.
photomerge.setupConnectivity = function()
{
	var i;
	
	// See if stackElem is connected to the "base".  If "without"
	// is given, then the path to the base must not use "without"
	function isConnectedToBase( stackElem, without, dbg_count )
	{
		if (typeof(dbg_count) == "undefined")
			dbg_count = 0;
			
		if (typeof(without) == "undefined")
			without = null;
			
		$.bp( dbg_count > 150 );	// oops, got stuck in a loop...

		if (stackElem == null)
			return false;
			
		if (stackElem == without)
			return false;
			
		if (stackElem.fConnectedTo == stackElem)
			return true;		// Already at base
		return isConnectedToBase( stackElem.fConnectedTo, without, dbg_count + 1 );
	}
	
	// Initialize
	for (i in this.stackElements)
	{
		this.stackElements[i].fBoundsCache = this.stackElements[i].getBounds();
		this.stackElements[i].fLayerID = i;
		// Bases connect to themselves.
		this.stackElements[i].fConnectedTo = this.stackElements[i].fBaseFlag ? this.stackElements[i] : null;
		this.stackElements[i].fNeighborOverlap = 0;
	}
		
	// Create a connection table based on the overlap of the pieces
	var g, i, j, baseInd = -1;
	if (this.fGroups)
		for (g in this.fGroups)
		{
			var group = this.fGroups[g];
			var connections = new Array();
			var numLayers = group.layers.length;
			
			// Ignore orphan images
			if (numLayers < 2)
			{
				group.layers[0].fBaseFlag = false;
				group.layers[0].fConnectedTo = null;
				continue;
			}
			// If there's no perspective, there's no connections ("0" is kProjective in UAlignment.h)
			if (group.xformType != 0)
			{
				for (i = 0; i < numLayers; ++i)
				{
					group.layers[i].fBaseFlag = false;
					group.layers[i].fConnectedTo = group.layers[i];
				}
				continue;
			}
			
			for (i = 0; i < numLayers; ++i)
			{
				if (group.layers[i].fBaseFlag)
					baseInd = i;
				connections[i] = new Array();
				group.layers[i].fGroupIndex = i;
				if (i > 0)
					for (j = 0; j < i; ++j)
					{
						connections[i][j] = group.layers[i].overlapArea( group.layers[j] );
						connections[j][i] = connections[i][j];	// table is symentric
					}
			}
			
			$.bp( baseInd == -1 );	// Never found the base?
			
			// Debug - dump the connection table
	/*		for (i = 0; i < numLayers; ++i)
			{
				var s = "";
				for (j = 0; j < numLayers; ++j)
					s += ", " + Math.floor(connections[i][j]);
				$.writeln(s);
			}
	*/		
			// Connect everything to the base that's connected.
			for (i = 0; i < numLayers; ++i)
				if ((i != baseInd) && (connections[baseInd][i] > 0))
				{
					group.layers[i].fConnectedTo = group.layers[baseInd];
					group.layers[i].fNeighborOverlap = connections[baseInd][i];
				}
					
			// Walk the cconnectivity table and make sure everything is
			// "optimally" connected.
			var changes = false;
			do {
				changes = false;
				for (i = 0; i < numLayers; ++i)
				{
					if (i != baseInd)
					{
						var curLayer = group.layers[i];
							
						for (j = 0; j < numLayers; ++j)
						{
							if (((j != baseInd) && (j != i))
								&& (((connections[i][j] > curLayer.fNeighborOverlap)
										&& isConnectedToBase( group.layers[j], curLayer ))))
							{
								curLayer.fConnectedTo = group.layers[j];
								curLayer.fNeighborOverlap = connections[i][j];
								changes = true;
							}
						}
					}
				}
			} while (changes);
			
	//		for (i = 0; i < numLayers; ++i)
	//			$.writeln( group.layers[i].fName + " is connected to " + (group.layers[i].fConnectedTo ? group.layers[i].fConnectedTo.fName : "??") );
		}
}

photomerge.offsetStack = function( delta )
{
	for (i in this.stackElements)
		this.stackElements[i].offset( delta );
	this.fBounds.offset( delta );
}

photomerge.scaleStack = function( s )
{
	for (i in this.stackElements)
		this.stackElements[i].scale( s );
	this.getBounds();
}

// This gets executed before a filter plugin is invoked.  "desc"
// allows passing parameters to the filter.
photomerge.customPluginArguments = function( desc )
{
	var f = new File(this.stackElements[0].fFullName);
	var path = File.encode( f.parent.fsName ) + (File.fs == "Windows" ? "\\" : "/");
	desc.putString( app.charIDToTypeID('PMfp'), path );
	
	if (this.compositionFile)
        {
		desc.putString( app.charIDToTypeID('PMrf'), this.compositionFile.fsName );
		desc.putString( app.charIDToTypeID('PMcf'), File.encode( this.compositionFile.fsName ) );
        }
}

photomerge.callInteractivePlugin = function( stackDoc )
{
	// Scale the results to fit the screen
/*		var scaleFactor = 1.0, screenSize = primaryScreenDimensions() * 0.75;
	
	if (this.fBounds.getHeight() > this.fBounds.getWidth())
	{
		if (this.fBounds.getHeight() > screenSize.fY)
			scaleFactor = screenSize.fY / this.fBounds.getHeight();
	}
	else
	{
		if (this.fBounds.getWidth() > screenSize.fX)
			scaleFactor = screenSize.fX / this.fBounds.getWidth();
	}
*/

	const kMaxPieceSize = 1024;		// Must match value in PhotomergeUI.cpp
	var i;
	// The old plugin insists on eight bit data.
	if (stackDoc.bitsPerChannel != BitsPerChannelType.EIGHT)
	{
		stackDoc.bitsPerChannel = BitsPerChannelType.EIGHT;
		this.stackDepthChanged = true;
	}
		
	if (this.compositionFile == null)
	{
		// Make sure the quad coordinates coorespond to the scale used by the UI plugin
		var maxPieceSize = 0;
		for (i in this.stackElements)
			maxPieceSize = Math.max( Math.max( this.stackElements[i].fWidth, this.stackElements[i].fHeight ), maxPieceSize );
		
		var mipLevel = 0;
		while (maxPieceSize >> mipLevel > kMaxPieceSize)
			mipLevel++;
			
		var imageReduction = 1.0 / (1 << mipLevel);
		this.offsetStack( -this.fBounds.getCenter() );
		this.scaleStack( imageReduction );
		this.offsetStack( -this.fBounds.getTopLeft() );
			
		this.setupConnectivity();
		
		// Add the additional per-piece metadata to pass to the filter plugin
		for (i in this.stackElements)
			this.stackElements[i].addPieceData();
	}
		
	// Make the result layer active
	app.activeDocument.activeLayer = app.activeDocument.layers[app.activeDocument.layers.length -1];
	
	// Note: we need an "unmodified" flag, so if no
	// changes are made we skip the data recovery step...
	var result, err;
	try {
		result = this.invokeFilterPlugin( "AdobePhotomergeUI4SCRIPT", DialogModes.ALL );
	}
	catch (err)
	{
		result = null;
	}
	
	if (result == null)		// Cancelled / bombed out
	{
		stackDoc.close(SaveOptions.DONOTSAVECHANGES);
		return null;
	}

	// Extract the data from the plugin
	var modifiedPieceInfo = result.getString( app.charIDToTypeID('PSpc') ).split('\n');
	for (i in modifiedPieceInfo)
	{
		// If we loaded a composition (.pmg) file, we won't have corners yet.
		if (typeof(this.stackElements[i].fCorners) == "undefined")
			this.stackElements[i].fCorners = new Array();
		var j, pieceData = modifiedPieceInfo[i].split('\t');
		for (j in pieceData)
		{
			var k, pair = pieceData[j].split('=');
			if (pair[0] == 'fUsed')
				this.stackElements[i].fUsed = eval(pair[1]);
			if (pair[0] == 'fCorners')
			{
				var coords = pair[1].split(/\s+/).slice(1);
				for (k = 0; k < 4; k++)
					this.stackElements[i].fCorners[k] = new TPoint( Number(coords[k*2]), Number(coords[k*2+1]) );
			}
		}
	}
	
	// Remove unused photos
	for (i = 0; i < this.stackElements.length; ++i)
		if (! this.stackElements[i].fUsed)
		{
			stackDoc.layers[(this.stackElements.length-1)-i].remove();
			this.stackElements.splice(i,1);
			i--;
		}
			
	// Hey...it could happen.
	if (this.stackElements.length < 2)
		return null;
	
	// If we run the UI, we're restricted to an eight bit stack.
	// If the source images were higher, we need to reload the image stack.
	if (this.stackDepthChanged)
	{
		stackDoc.close(SaveOptions.DONOTSAVECHANGES);
		stackDoc = this.loadStackLayers();
	}
	this.getBounds();		// Update w/new corner data
	return stackDoc;
}

// "Adobe patent application tracking # B349, entitled 'Method and apparatus for Layer-based Panorama Adjustment and Editing', inventor: John Peterson"
//
// Use the geometry of the overlapping pieces to create
// simple rectangular blend masks.
//
photomerge.quickBlend = function()
{
	var i, j;

	var blendRadius = Math.round(Math.min(this.stackElements[0].fWidth, this.stackElements[0].fHeight) / 10.0);
	
	if (PMDebug)
		for (i in this.stackElements)
			this.stackElements[i].dumpMLCorners();
			
	// Set up progress bar for blending
	// The progress bar doesn't work - there's know way in ScriptUI to force it to update.
/*	var progressWindow = latteUI( g_StackScriptFolderPath + "PMBlendingProgress.exv" );
	var num = this.stackElements.length;
	var progressBar = progressWindow.findControl('_progress');
	progressBar.maxvalue = (num * (num + 1)) / 2;
	num = 0;
	progressWindow.center();
	progressWindow.show();
*/	
	// Blend the i'th piece against the 0..i-1 pieces below it
	for (i = this.stackElements.length-1; i > 0; --i)
		for (j = i-1; j >= 0; j--)
		{
//			num++;
//			progressBar.value = num; // ScriptUI bug - there's no way to force this to update.
			this.stackElements[i].softEdgeBlend( this.stackElements[j], blendRadius );
		}
//	progressWindow.close();
}

// Wrap the advancedBlend in a try/catch so errors (i.e., user cancel)
// just stop the blend process.
photomerge.advancedBlend = function( stackDoc )
{
	try {
		selectAllLayers(stackDoc, 1);
		advancedMergeLayers();
	}
	catch (err)
	{
	}
}

// With the stack elements specified, this
// portion actually creates the Panorama.
// Returns boolean indicating success/failure
photomerge.doPanorama = function()
{
	var i, stackDoc = null;
	
	function primaryScreenDimensions()
	{
		var i;
		for (i in $.screens)
			if ($.screens[i].primary)
				return new TPoint( $.screens[i].right - $.screens[i].left, 
									  $.screens[i].bottom - $.screens[i].top );
	} 
	
	if (this.interactiveFlag)
	{
		// Filter must have eight bit depth and RGB color space
		stackDoc = this.loadStackLayers( BitsPerChannelType.EIGHT );
		stackDoc.changeMode(ChangeMode.RGB);
	}
	else
		stackDoc = this.loadStackLayers();

	if (! stackDoc)
		return false;
		
	// Remove spurious last layer (not needed by Photomerge)
	if (app.activeDocument.layers[app.activeDocument.layers.length-1].name == this.pluginName)
		app.activeDocument.layers[app.activeDocument.layers.length-1].remove();

	// The UI needs everything in the top left corner
	if (this.interactiveFlag)
		for (i = 0; i < stackDoc.layers.length; ++i)
		{
			var xoff = stackDoc.layers[i].bounds[0].as("px");
			var yoff = stackDoc.layers[i].bounds[1].as("px");
			if ((xoff != 0) || (yoff != 0))
				stackDoc.layers[i].translate( UnitValue( -xoff, "px" ), UnitValue( -yoff, "px" ) );
		}
	
	// Sort out exactly what operations we want to do.
	if (! this.interactiveFlag)
	{
		selectAllLayers(stackDoc, 1);
		this.getAlignmentInfo( stackDoc, true );
		if (this.advancedBlending)
		{
			stackDoc.changeMode( ChangeMode.RGB );	// Auto-blend requires  RGB
			this.advancedBlend( stackDoc );
		}
// The Advanced blending works so well that there's little point
// in having the rectangular gradient blends anymore.  Uncomment the
// following two lines if you still want them (see similar code below
// for the interactive case).
//		else
//			this.quickBlend();
		purgeHistoryStates();
		return true;
	}

	// Interactive happens here
	if (this.compositionFile == null)
	{
		this.getAlignmentInfo( stackDoc, false );	
	
		// With the corners computed by getAlignmentInfo,
		// find the bounds and use that to slide the images
		// over so their bounds has origin 0,0 (top left)
		this.offsetStack( -this.fBounds.getTopLeft() );
	}
	
	if (this.interactiveFlag)
		stackDoc = this.callInteractivePlugin( stackDoc );
	if (stackDoc == null)
		return false;
	
	// Extend the canvas to hold the panorama
	var w = UnitValue( this.getBounds().getWidth(), "px" );
	var h = UnitValue( this.getBounds().getHeight(), "px" );
	app.activeDocument.resizeCanvas( w, h, AnchorPosition.TOPLEFT );

	// Now apply the transformation to the pieces
	for (i in this.stackElements)
		this.stackElements[i].transform();

	if (this.advancedBlending)
	{
		selectAllLayers( stackDoc, 1 );
		stackDoc.changeMode( ChangeMode.RGB );	// Auto-blend requires  RGB
		advancedMergeLayers();
	}
// The new "advanced blending" works well enough that there's
// little point in invoking the rectangular gradient blends anymore.
// If you really want them, you can uncomment the two lines below.
//	else
//		this.quickBlend();

	purgeHistoryStates();
	return true;
}

// Extra group breaks the main dialog's radio buttons,
// so we manually implement it here (ScriptUI lossage)
// NOTE: When called, this is a member function of radioControl,
// -not- photomerge.  JavaScript voodoo.
photomerge.radioClick = function()
{
	var w = this.window;
	var i;

	for (i in photomerge.radioButtons)
	{
		var b = w.findControl(photomerge.radioButtons[i]);
		
		if (b != this)
			b.value = false;
	}
}

// Callback when "Load Composition" is clicked.
// NOTE: When called, this is a member function of buttonControl,
// -not- photomerge.  JavaScript voodoo.
photomerge.loadCompositionClick = function()
{
	function MacPMGType( f )
	{
		if (f.type == 'PhMg')
			return true;
		var match = f.name.match(/.*[.](.+)$/);
		var suffix = match != null ? match[1].toLowerCase() : "";
		if (suffix == "pmg")
			return true;
		if (f instanceof Folder)
		{
			// If the item is an app or a bundle it will be an 
			// instance of folder. If we return true it will 
			// appear as an enabled item. While the OS will not
			// let the user navigate into it, it is better to 
			// have it appear as disabled.
			if (suffix.match(/app|bundle/i) != null)
			{
				// Do not navigate folders that end in .app or .bundle
				return false;
			}
			// navigate any other folder
			return true;
		}
		// some unknown file type/suffix
		return false;
	}

	var fileType = File.fs == "Windows" ? localize("$$$/AdobePlugin/Shared/Photomerge/Auto/Win=Photomerge Compositions:*.pmg") 
									: MacPMGType;
									
	photomerge.compositionFile = File.openDialog( localize("$$$/AdobePlugin/Photomerge/LoadComp=Load Photomerge Composition"), fileType );
	if (photomerge.compositionFile == null || !photomerge.compositionFile.open("r"))
		return;
		
	var line = photomerge.compositionFile.readln();
	if (! line.match(/^VIS/))
	{
		alert( this.pluginName + localize("$$$/AdobePlugin/Photomerge/BadComp=The Composition file is corrupt"), this.pluginName, true );
		return;
	}
	
	// Read through the composition file and extract the file paths in it.
	var mergeFiles = new Array();
	while (! photomerge.compositionFile.eof)
	{
		line = photomerge.compositionFile.readln();
		var f = line.match(/^\s*PATH\s+<([^>]+)/);
		if (f)
		{
			// If no file path delimiters, image paths are assumed relative to the composition file
			var relPath = (f[1].indexOf( (File.fs == "Windows") ? "\\" : "/" ) < 0);
			f = (relPath ? photomerge.compositionFile.path + "/" : "") + f[1];
			mergeFiles.push( new StackElement( new File(f) ) );
		}
	}
	
	if (mergeFiles.length < 2)
	{
		alert( this.pluginName + localize("$$$/AdobePlugin/Photomerge/BadComp=The Composition file is corrupt"), this.pluginName, true );
		return;
	}

	photomerge.stackElements = mergeFiles;
	photomerge.interactiveFlag = true;
	this.window.close(kFilesFromPMLoad);
}

// Set up the radio buttons
photomerge.customDialogSetup = function( w )
{
	var i;
	for (i in this.radioButtons)
		w.findControl(this.radioButtons[i]).onClick = this.radioClick;
		
	w.findControl("_loadcomp").onClick = this.loadCompositionClick
		
	w.findControl("_LOauto").value = true;		// Set default
	w.findControl("_advancedBlend").value = this.advancedBlending;
}

// Called by the dialog on closing to collect the results
photomerge.customDialogFunction = function( w )
{
	if (w.findControl("_LOinteractive").value)
	{
		this.interactiveFlag = true;
		this.alignmentKey = 'interactive';
	}
	else
	{
		var i, d = [{k:"_LOauto",v:"Auto"},{k:"_LOnormal",v:"translation"},{k:"_LOperspective",v:"Prsp"},{k:"_LOcylindrical",v:"cylindrical"}];
		for (i in d)
			if (w.findControl(d[i].k).value)
			{
				this.alignmentKey = d[i].v;
				break;
			}
	}
	this.advancedBlending = w.findControl("_advancedBlend").value;
}

// "Main" execution of Photomerge from the menu
photomerge.doInteractivePano = function ()
{
	// Because of the ",true", the dialog is pre-loaded with any bridge files.
	this.getFilesFromBridgeOrDialog( localize("$$$/Private/Photomerge/PMDialogexv=PMDialog.exv"), true );

	try {
		if (this.stackElements && this.doPanorama())
		{
			fitViewOnScreen();
			var flagDesc = new ActionDescriptor();
			flagDesc.putBoolean( kPhotomergeAdvancedBlendingFlag, photomerge.advancedBlending );
			app.putCustomOptions( "PhotomergeFlags001", flagDesc, true );
		}
	}
	catch (err)
	{
		if (this.stackDoc)
			this.stackDoc.close(SaveOptions.DONOTSAVECHANGES)
	}
}

// Use this version to call Photomerge from a script.
photomerge.createPanorama = function(filelist, displayDialog)
{
	this.interactiveFlag = (typeof(displayDialog) == 'boolean') ? displayDialog : false;
		
	if (filelist.length < 2)
	{
		alert(localize("$$$/AdobePlugin/Shared/Photomerge/AtLeast2=Photomerge needs at least two files selected."), this.pluginName, true );
		return;
	}
	var j;
	this.stackElements = new Array();
	for (j in filelist)
	{
		var f = filelist[j];
		this.stackElements.push( new StackElement( (typeof(f) == 'string') ? File(f) : f ) );
	}
		
	if (this.stackElements.length > 1)
		this.doPanorama();
}

if ((typeof(runphotomergeFromScript) == 'undefined')
	|| (runphotomergeFromScript==false))
	photomerge.doInteractivePano();
