﻿//------------------------------------------------------------------------------
//
// ADOBE SYSTEMS INCORPORATED
// Copyright 2008 Adobe Systems Incorporated 
// All Rights Reserved
//
// NOTICE: Adobe permits you to use, modify, and distribute 
// this file in accordance with the terms of the Adobe license 
// agreement accompanying it. If you have received this file 
// from a source other than Adobe, then your use, modification, 
// or distribution of it requires the prior written permission 
// of Adobe.
//
//------------------------------------------------------------------------------

/*
@@@BUILDINFO@@@ AutoCollection.jsx 3.0.0.2
*/

//
// AutoStacks.jsx - Automatically locate and create panoramas and HDR sets from a folder of images.
//

// Need localizations for 
/*
@@@START_XML@@@
<?xml version="1.0" encoding="UTF-8"?>
<ScriptInfo xmlns:dc="http://purl.org/dc/elements/1.1/" xml:lang="en_US">
     <dc:title>Auto Collection CC</dc:title>
     <dc:description>This adds features to automatically collect and process sets of images forming panoramas and High Dynamic Range images</dc:description>
</ScriptInfo>
<ScriptInfo xmlns:dc="http://purl.org/dc/elements/1.1/" xml:lang="de_DE">
     <dc:title>Auto Collection CC</dc:title>
     <dc:description>Hiermit werden Funktionen hinzugefügt, die das automatische Zusammenstellen und Verarbeiten von Bildsätzen für Panoramen und HDR-Bilder (High Dynamic Range) ermöglichen</dc:description>
</ScriptInfo>
<ScriptInfo xmlns:dc="http://purl.org/dc/elements/1.1/" xml:lang="fr_FR">
     <dc:title>Auto Collection CC</dc:title>
     <dc:description>Cette action permet d'ajouter des fonctions qui recueillent et traitent automatiquement les ensembles d'images pour former des panoramas et des images HDR (High Dynamic Range)</dc:description>
</ScriptInfo>
<ScriptInfo xmlns:dc="http://purl.org/dc/elements/1.1/" xml:lang="fr_CA">
     <dc:title>Auto Collection CC</dc:title>
     <dc:description>Cette action permet d'ajouter des fonctions qui recueillent et traitent automatiquement les ensembles d'images pour former des panoramas et des images HDR (High Dynamic Range)</dc:description>
</ScriptInfo>
<ScriptInfo xmlns:dc="http://purl.org/dc/elements/1.1/" xml:lang="fr_XM">
     <dc:title>Auto Collection CC</dc:title>
     <dc:description>Cette action permet d'ajouter des fonctions qui recueillent et traitent automatiquement les ensembles d'images pour former des panoramas et des images HDR (High Dynamic Range)</dc:description>
</ScriptInfo>
<ScriptInfo xmlns:dc="http://purl.org/dc/elements/1.1/" xml:lang="ja_JP">
     <dc:title>Auto Collection CC</dc:title>
     <dc:description>パノラマを構成する画像やハイダイナミックレンジ画像を自動的に収集し、作成するための機能を追加します</dc:description>
</ScriptInfo>
<ScriptInfo xmlns:dc="http://purl.org/dc/elements/1.1/" xml:lang="da_DK">
     <dc:title>Auto Collection CC</dc:title>
     <dc:description>Dette tilføjer funktioner, der automatisk indsamler og behandler sæt af billeder til panoramabilleder eller HDR-billeder (High Dynamic Range)</dc:description>
</ScriptInfo>
<ScriptInfo xmlns:dc="http://purl.org/dc/elements/1.1/" xml:lang="es_ES">
     <dc:title>Auto Collection CC</dc:title>
     <dc:description>Añade funciones para recopilar y procesar conjuntos de imágenes que forman panoramas e imágenes de alto rango dinámico de una forma automática</dc:description>
</ScriptInfo>
<ScriptInfo xmlns:dc="http://purl.org/dc/elements/1.1/" xml:lang="es_MX">
     <dc:title>Auto Collection CC</dc:title>
     <dc:description>Añade funciones para recopilar y procesar conjuntos de imágenes que forman panoramas e imágenes de alto rango dinámico de una forma automática</dc:description>
</ScriptInfo>
<ScriptInfo xmlns:dc="http://purl.org/dc/elements/1.1/" xml:lang="fi_FI">
     <dc:title>Auto Collection CC</dc:title>
     <dc:description>Tämä lisää ominaisuudet, joiden avulla voidaan koota ja käsitellä panoraamat ja High Dynamic Range -kuvat muodostavat kuvajoukot</dc:description>
</ScriptInfo>
<ScriptInfo xmlns:dc="http://purl.org/dc/elements/1.1/" xml:lang="it_IT">
     <dc:title>Auto Collection CC</dc:title>
     <dc:description>Aggiunge funzioni per raccogliere ed elaborare automaticamente serie di immagini per immagini panoramiche e High Dynamic Range</dc:description>
</ScriptInfo>
<ScriptInfo xmlns:dc="http://purl.org/dc/elements/1.1/" xml:lang="nb_NO">
     <dc:title>Auto Collection CC</dc:title>
     <dc:description>Dette legger til funksjoner for automatisk innsamling og behandling av sett med bilder som danner panoramaer og bilder av typen High Dynamic Range</dc:description>
</ScriptInfo>
<ScriptInfo xmlns:dc="http://purl.org/dc/elements/1.1/" xml:lang="nl_NL">
     <dc:title>Auto Collection CC</dc:title>
     <dc:description>Zo voegt u functies toe voor het automatisch verzamelen en verwerken van afbeeldingensets die panorama's en HDR-afbeeldingen vormen</dc:description>
</ScriptInfo>
<ScriptInfo xmlns:dc="http://purl.org/dc/elements/1.1/" xml:lang="pt_BR">
     <dc:title>Auto Collection CC</dc:title>
     <dc:description>Isto adiciona recursos para coletar e processar automaticamente conjuntos de imagens, formando panoramas e imagens High Dynamic Range</dc:description>
</ScriptInfo>
<ScriptInfo xmlns:dc="http://purl.org/dc/elements/1.1/" xml:lang="sv_SE">
     <dc:title>Auto Collection CC</dc:title>
     <dc:description>Detta lägger till funktioner för att automatiskt samla in och bearbeta bilduppsättningar och göra panoraman och High Dynamic Range-bilder</dc:description>
</ScriptInfo>
<ScriptInfo xmlns:dc="http://purl.org/dc/elements/1.1/" xml:lang="ko_KR">
     <dc:title>Auto Collection CC</dc:title>
     <dc:description>파노라마 및 HDR(High Dynamic Range) 이미지를 구성하는 이미지 세트를 자동으로 모으고 처리하는 기능을 추가합니다</dc:description>
</ScriptInfo>
<ScriptInfo xmlns:dc="http://purl.org/dc/elements/1.1/" xml:lang="zh_CN">
     <dc:title>Auto Collection CC</dc:title>
     <dc:description>这会添加功能以自动收集和处理形成全景图和高动态范围图像的图像组</dc:description>
</ScriptInfo>
<ScriptInfo xmlns:dc="http://purl.org/dc/elements/1.1/" xml:lang="zh_TW">
     <dc:title>Auto Collection CC</dc:title>
     <dc:description>這會新增功能以自動收集並處理形成全景和高動態範圍影像的影像集</dc:description>
</ScriptInfo>
<ScriptInfo xmlns:dc="http://purl.org/dc/elements/1.1/" xml:lang="cs_CZ">
     <dc:title>Auto Collection CC</dc:title>
     <dc:description>Přidává funkce pro automatické shromažďování a zpracování sad obrazů, které tvoří panoramata a obrazy s vysokým dynamickým rozsahem</dc:description>
</ScriptInfo>
<ScriptInfo xmlns:dc="http://purl.org/dc/elements/1.1/" xml:lang="hu_HU">
     <dc:title>Auto Collection CC</dc:title>
     <dc:description>Ez a lehetőség a panorámaképeket alkotó, illetve a nagy dinamikai tartománnyal bíró (HDR) képek automatikus begyűjtésére és feldolgozására alkalmas funkciókkal bővíti a funkciók körét</dc:description>
</ScriptInfo>
<ScriptInfo xmlns:dc="http://purl.org/dc/elements/1.1/" xml:lang="pl_PL">
     <dc:title>Auto Collection CC</dc:title>
     <dc:description>Dodaje funkcje automatycznego zbierania i przetwarzania zestawów obrazów tworzących panoramy i obrazy HDR (High Dynamic Range)</dc:description>
</ScriptInfo>
<ScriptInfo xmlns:dc="http://purl.org/dc/elements/1.1/" xml:lang="ro_RO">
     <dc:title>Auto Collection CC</dc:title>
     <dc:description>Aceasta adaugă funcţii pentru colectarea şi procesarea automată a seturilor de imagini ce formează panorame şi imagini cu interval dinamic extins (HDR)</dc:description>
</ScriptInfo>
<ScriptInfo xmlns:dc="http://purl.org/dc/elements/1.1/" xml:lang="ru_RU">
     <dc:title>Auto Collection CC</dc:title>
     <dc:description>Эта операция добавляет функции автоматического сбора и обработки наборов изображений для создания панорам и изображений с широким динамическим диапазоном</dc:description>
</ScriptInfo>
<ScriptInfo xmlns:dc="http://purl.org/dc/elements/1.1/" xml:lang="tr_TR">
     <dc:title>Auto Collection CC</dc:title>
     <dc:description>Bu, panorama ve Yüksek Dinamik Aralıklı (HDR) görüntüler oluşturan görüntü kümelerini otomatik olarak toplama ve işleme özellikleri ekler</dc:description>
</ScriptInfo>
<ScriptInfo xmlns:dc="http://purl.org/dc/elements/1.1/" xml:lang="uk_UA">
     <dc:title>Auto Collection CC</dc:title>
     <dc:description>Цей сценарій містить додаткові функції для автоматичного збирання і обробки комплектів зображень, які формують панораму або зображення з високим динамічним діапазоном</dc:description>
</ScriptInfo>
<ScriptInfo xmlns:dc="http://purl.org/dc/elements/1.1/" xml:lang="el_GR">
     <dc:title>Auto Collection CC</dc:title>
     <dc:description>Προσθέτει λειτουργίες για την αυτόματη συλλογή και επεξεργασία σετ εικόνων που δημιουργούν πανοράματα και εικόνων υψηλού δυναμικού εύρους</dc:description>
</ScriptInfo>
<ScriptInfo xmlns:dc="http://purl.org/dc/elements/1.1/" xml:lang="ar_AE">
     <dc:title>Auto Collection CC</dc:title>
     <dc:description>يضيف هذا مزايا لتجميع ومعالجة مجموعات من الصور تشكل بانوراما</dc:description>
</ScriptInfo>
<ScriptInfo xmlns:dc="http://purl.org/dc/elements/1.1/" xml:lang="he_IL">
     <dc:title>Auto Collection CC</dc:title>
     <dc:description>מוסיף תכונות לאיסוף ולעיבוד אוטומטיים של אוספי תמונות היוצרים תמונות פנורמה ותמונות HDR ‏(High Dynamic Range)</dc:description>
</ScriptInfo>
<ScriptInfo xmlns:dc="http://purl.org/dc/elements/1.1/" xml:lang="en_AE">
     <dc:title>Auto Collection CC</dc:title>
     <dc:description>This adds features to automatically collect and process sets of images forming panoramas and High Dynamic Range images</dc:description>
</ScriptInfo>
<ScriptInfo xmlns:dc="http://purl.org/dc/elements/1.1/" xml:lang="en_IL>
     <dc:title>Auto Collection CC</dc:title>
     <dc:description>This adds features to automatically collect and process sets of images forming panoramas and High Dynamic Range images</dc:description>
</ScriptInfo>
<ScriptInfo xmlns:dc="http://purl.org/dc/elements/1.1/" xml:lang="fr_MA">
     <dc:title>Auto Collection CC</dc:title>
     <dc:description>Cette action permet d'ajouter des fonctions qui recueillent et traitent automatiquement les ensembles d'images pour former des panoramas et des images HDR (High Dynamic Range)</dc:description>
</ScriptInfo>
@@@END_XML@@@
*/



#target bridge

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

// If more than this many files have the same metadata, then
// stop; something is seriously wrong with the dataset.
var kMaxSameMetadata = 60;

var kHDRKey = "HDR";
var kPanoramaKey = "panorama";
var AdobePatentID="B863"
// AdobePatentID="B863"

///==============================================================================

// This section defines the criteria used for determining if a photo
// is part of a Panorama or HDR collection.

function BaseMatcher(name)
{
	// The time stamp (DateTimeDigitized) on the photos must be
	// AT LEAST this close to be considered for the auto-collection.
	// If you wish this value to be different for HDR sets than it is
	// for Panoramas, see below.
	this.kTimeDelta = 18;
	
	// The focal length computed by the alignment library must be within
	// at least this fraction to be considered (i.e., "0.1" == 10%)
	this.kMaxFLDelta = 0.1;
	this.collectionType = name;
}

panoramaMatcher = new BaseMatcher(kPanoramaKey);

// For panoramas, the EV (exposure value) must change by LESS than
// this amount to be accepted.
panoramaMatcher.kEVDelta = 2.0;

// For panoramas, all of the photos together must have -less- overlap than this
// amount (default is 0.80 == "80%".  If all of the photos cover 80% of the same
// spot, it's probably not a panorama.
panoramaMatcher.kMinOverlap = 0.80;


HDRMatcher = new BaseMatcher(kHDRKey);

// For an HDR set, all of the photos must overlap by -at least- this amount.
HDRMatcher.kMinOverlap = 0.85;

// For an HDR set, the EV (exposure value) must differ by AT LEAST this
// amount to be accepted into the collection.
HDRMatcher.kEVDelta = 0.8;

// Uncomment the line below if you want a time delta for HDR sets
// that's different than the default set above
//HDRMatcher.kTimeDelta = 15;

//==============================================================================

// Weed out images that don't match with the SIFT code. Modifies imgSet in place
BaseMatcher.prototype.scrubAlignedImages = function( alignInfo, imgSet )
{
	var i;

	function removeFromSet(i, why)
	{
		imgSet[i].cachedCollectionType = null;	// Mark as available again
//		$.writeln('removed ('+why+'): ' + imgSet[i].name);
		imgSet.splice(i,1);
		alignInfo.overlap.splice(i,1);
		alignInfo.fl.splice(i,1);
		alignInfo.sheetID.splice(i,1);
	}

	// Find the minimum computed focal length
	minCFL = 1e37;
	for (i in imgSet)
		if ((alignInfo.fl[i] > 0) && (alignInfo.fl[i] < minCFL))
			minCFL = alignInfo.fl[i];
	
	for (i = imgSet.length - 1; i >= 0; --i)
	{
		if (! alignInfo.renderable[i])
			removeFromSet( i, 'non-render' );
		else
		if ((alignInfo.sheetID[i] != alignInfo.refSheetID) // Never reject ref sheet.
			&& (this.rejectOverlap(alignInfo.overlap[i])))
			removeFromSet( i, 'overlap' );
		else
		if ((alignInfo.fl[i] > 0) && (((alignInfo.fl[i] - minCFL)/ minCFL) > this.kMaxFLDelta))
			removeFromSet( i, 'fl' );
	}

	// Flag the collection type
	if (imgSet.length > 1)
	{
		for (i in imgSet)
			imgSet[i].cachedCollectionType = this.collectionType;
	}
	else
	{
		if (imgSet.length) imgSet[0].cachedCollectionType = null;
		imgSet.length = 0;
	}
}

panoramaMatcher.matchMetadata = function( thumb1, thumb2 )
{
	return ((thumb2.cachedTime - thumb1.cachedTime < this.kTimeDelta)
			&& (thumb1.cachedFL == thumb2.cachedFL)
			&& (Math.abs(thumb1.cachedEV - thumb2.cachedEV) < this.kEVDelta));
}

panoramaMatcher.rejectOverlap = function( minOverlap )
{
	return (minOverlap > this.kMinOverlap);
}

//==============================================================================

HDRMatcher.matchMetadata = function( thumb1, thumb2 )
{
	return ((thumb2.cachedTime - thumb1.cachedTime < this.kTimeDelta)
			&& (thumb1.cachedFL == thumb2.cachedFL)
			&& (Math.abs(thumb1.cachedEV - thumb2.cachedEV) >= this.kEVDelta));
}

// Weed out images that don't match with the SIFT code. Modifies imgSet in place
HDRMatcher.rejectOverlap = function( minOverlap )
{
	return (minOverlap < this.kMinOverlap);	// Too much displacement
}

//==============================================================================

// Alignment library management
//-----------------------------------------------------------------------------------------------------------------------

// So, I can create my object as an extension library (see JS Tools Guide,
// "Integrating External Libraries"), and use the script to feed points to the
// bitmap data into AlignmentLib.

function AlignmentLibrary()
{
	if ($.version.search(/debug/) > 0)
		this.runTimeMode = "debug";
	else
		this.runTimeMode = "release";
	this.cpp_code = null;
	this.loadLibraryFailed = false;
}

// This allows reloading if we're debugging, since reloading the External Libraries is a mess.
if (typeof(alignmentLib) == "undefined")
	alignmentLib = new AlignmentLibrary();

alignmentLib.rtVersion = function(str)
{
	return str.replace(/-RTM-/, this.runTimeMode);
}

// Load and initialize the libraries - Developer version.  This pulls the libraries from
// where they're built.
alignmentLib.devLoadLibrary = function()
{

	// The DLLTools library is a stop-gap required to make the call to the windows SetDllDirectory routine.
	// This tells the Windows loader where to find libraries AlignmentLib subsequently loads.
	// In the final build, this shouldn't be necessary because they'll all be sitting along side the application,
	// where the loader looks by default.  "-RTM-" is replaced by "Release" or "Debug"

	if (this.cpp_code)
		return;			// Already loaded

    var pluginPath, alignmentLibraryPath;
    
	if (File.fs == "Windows")
	{
		var appFolder = File(BridgeTalk.getAppPath(BridgeTalk.getSpecifier("bridge"))).path;
		var extensionPath = Folder(appFolder + "/Plug-Ins").fsName
		alignmentLibraryPath = "/d/Work/plugins/Targets/Win32/Debug/Libraries";
		
//		var dllToolPath = "lib:\\D\\Work\\AlignmentLib\\DLLTools\\Debug\\DLLTools.dll";
//		var dllLib = new ExternalObject( this.rtVersion(dllToolPath) );
//		dllLib.setDLLDirectory(File(alignmentLibraryPath).fsName);

	//	alert('Dev load! '+ alignmentLibraryPath);
		this.cpp_code = new ExternalObject("lib:" + File(alignmentLibraryPath +"/AlignmentLib.dll").fsName, extensionPath );
	}
	if (File.fs == "Macintosh")
	{
		pluginPath =  "/Applications/Adobe Photoshop CC/Adobe Photoshop CC.app/Contents/Required/Plug-Ins/Extensions";
		alignmentLibraryPath = "/Work/plugins/Targets/Debug_x86_64/Libraries/AlignmentLib.framework";
        this.cpp_code = new ExternalObject("lib:" + alignmentLibraryPath, pluginPath );
	}
	$.writeln("Dev library " + alignmentLibraryPath + (this.cpp_code ? " loaded." : " FAILED to load.") );
}

// Load and initialize the libraries - User version.  This pulls the libraries from where they're installed.
alignmentLib.userLoadLibrary = function()
{
	if (this.cpp_code)
		return;		// Already loaded
	
	try {
		var bridgeSpec = BridgeTalk.getSpecifier("bridge");
		var appFolder = File(BridgeTalk.getAppPath(bridgeSpec)).path;

		var acFolder;
		// Special case if 64 bit PS is installed, we need to get the alignmentLib from the 32 bit install
        // if we're running 32 bit also.
        var psSpec = BridgeTalk.getSpecifier("photoshop");
		if ((File.fs == "Windows") && (psSpec.slice(-2) == "64") && (bridgeSpec.slice(-2) == "32"))
		{
            var spec32 = psSpec.split(".")[0] + ".032";
			if (BridgeTalk.getAppPath(spec32) == "")
				acFolder = Folder( Folder.commonFiles + "/Adobe/Adobe Photoshop CC/32 bit Photoshop dlls" )
			else
				acFolder = File(BridgeTalk.getAppPath(spec32)).path;
		}
		else
			acFolder = File(BridgeTalk.getAppPath(psSpec)).path;
		
		var pluginFolder = Folder(appFolder + "/Plug-Ins");

		if (File.fs == "Windows")
		{
			// For some reason, the search path on Windows can't find this library, so we force it to load.
			var libifcore = new ExternalObject( "lib:" + File(acFolder + "/libifcoremd.dll").fsName );
			this.cpp_code = new ExternalObject( "lib:" + File(acFolder + "/AlignmentLib.dll").fsName,  pluginFolder.fsName );
		}
		if (File.fs == "Macintosh")
		{
			var fwFolder = Folder.decode(acFolder + "/" + Folder(acFolder).name + ".app") + "/Contents/Frameworks/AlignmentLib.framework";
			this.cpp_code = new ExternalObject("lib:" + fwFolder, pluginFolder.fsName );
		}
	}
	catch (error)
	{}	// Error reported below...the diagnostics from ES are useless anyway.
	
	if (! this.cpp_code)
	{
		alert( localize('$$$/BridgeExtension/AutoCollect/LibraryFailed=Unable to load AlignmentLib'));
		this.loadLibraryFailed = true;
	}
}
	
alignmentLib.loadLibrary = function()
{
	var useDevLoad = false;
	
	// Only do the developer load version on John's machine
	if (useDevLoad && ((($.getenv("COMPUTERNAME") == "MERGEPAD") && ($.getenv("USERNAME") == "jpeterso"))
		|| (($.getenv("HOME") == "/Users/jpeterso"))))
		this.devLoadLibrary();
	else
		this.userLoadLibrary();
}


// Runs the alignment library on the given set and returns the alignInfo struct
alignmentLib.getAlignmentInfo = function( candidateSet )
{
    function ptrToHex( ptr )
    {
        if (ptr.length == 2)    // 64 bit
        {
            low32 = ptr[1].toString(16).toUpperCase();
            low32 = "0000000".slice(0, 8-low32.length) + low32; // Add leading zeros to 32 bits
            return "0x" + ptr[0].toString(16).toUpperCase() + low32;
        }
        return "0x" + ptr.toString(16).toUpperCase();
    }
            
	var i;
	if (! this.cpp_code)
		this.loadLibrary();

	if (! this.cpp_code)
		throw("alignLibLoadFailed");

	for (i in candidateSet)
	{
		// This should really throw...
		if (! candidateSet[i].fileSystem.preview.hasHighQualityPreview)
			alert( localize('$$$/BridgeExtension/AutoCollect/MissingPreview=Image is missing preview data: ') + candidateSet[i].name );
		var preview = candidateSet[i].fileSystem.preview.preview;
		var img_ptr_string = ptrToHex( preview.pointer );
		this.cpp_code.addImage( preview.height, preview.width, preview.rowBytes, candidateSet[i].rotation,
								img_ptr_string );
	}

	// No argument or 1 = cylindrical; 0 = projective, -1 = auto.
	// NOTHING ELSE IS SUPPORTED.
	result = this.cpp_code.alignImages( -1 );
	return result;
}

alignmentLib.alignSelection = function()
{
	return alignmentLib.getAlignmentInfo( app.document.selections );
}

// Don't do this now, so we don't take a hit on startup time!
//alignmentLib.loadLibrary();

//==============================================================================

// Progress bar definition
//-----------------------------------------------------------------------------------------------------------------------

var progressRes = "dialog \
{\
	text: '$$$/AdobeScript/BridgeExtension/AutoCollection/Title=Auto Collection',\
	preferredSize: [450,-1],\
	view1: Group\
	{\
		alignment: 'fill', orientation: 'column',\
		_label: StaticText{ alignment: 'fill', text: '$$$/AdobeScript/BridgeExtension/ExamImages=Examining images...' },\
		_progress: Progressbar{ alignment: 'fill', properties: { name: 'progress' } },\
		_startAndCancel: Button{ text: '$$$/AdobeScript/BridgeExtension/AutoCollection/Cancel=Cancel      ',\
			started: false, cancelled: false,\
			properties: { name: 'cancel' }\
		},\
	}\
}";

function ProgressIndicator()
{
	this.window = new Window( localize(progressRes) );
	this.progressBar = this.window.children[0]['_progress'];
	this.window.progressBar = this.progressBar;
	this.window.progressBase = this;
	this.window.cancelButton = this.window.children[0][ '_startAndCancel'];
	this.window.cancelButton.cancelled = false;
	this.window.cancelButton.onClick = function()
	{
		this.cancelled = true;
		this.text = localize("$$$/AdobeScript/BridgeExtension/AutoCollection/Canceling=Canceling...");
	}

	this.window.onIdle = function()
	{
		if ((this.progressBar.value < this.progressBar.maxvalue) && ! this.cancelButton.cancelled)
		{
			this.progressBar.value = this.progressBar.value + 1;
			this.window.update();
			this.progressBase.workFunction();
        	this.window.notify('onIdle');
		} else {
            this.close();
        }
	}
}

ProgressIndicator.prototype.isCancelled = function()
{
	return this.window.cancelButton.cancelled;
}

ProgressIndicator.prototype.workFunction = function()
{
	$.sleep(500);
}

ProgressIndicator.prototype.step = function()
{
    this.window.progressBar.value = this.window.progressBar.value + 1;
    this.window.update();
}

ProgressIndicator.prototype.setMax = function( maxval )
{
	this.window.progressBar.maxvalue = maxval;
	this.window.progressBar.value = 0;
}

ProgressIndicator.prototype.setText = function( label )
{
	this.window.children[0]['_label'].text = label;
}

ProgressIndicator.prototype.start = function()
{
	this.window.notify('onIdle');
	this.window.show();
}

ProgressIndicator.prototype.fileMessage = function( msg, currentGroup )
{
	var lastName = currentGroup[currentGroup.length-1].name;
	this.setText( msg + currentGroup[0].name + ' - ' + lastName + ' (' + currentGroup.length + ')'  );
	this.window.update();
}

//==============================================================================

// Tags used for properties saved in the stacks
// The "@" allows it's use as an attribute (x[kXyzAttr] vs. x.@Xyz)
var kTypeAttr = "@collectiontype";
var kTimeAttr = "@timestamp";
var kPathAttr = "@path";
var kFlatViewAttr = "@flatview";

//==============================================================================

function AutoCollector()
{
	this.matcher = null;
	this.loadedComplete = false;
	this.candidateGroups = [];
	this.collectionGroups = [];
	this.cur = 0;
	this.panoPrefix = localize('$$$/BridgeExtensions/AutoCollect/PanoPrefix=Pano_');
	this.HDRPrefix = localize('$$$/BridgeExtensions/AutoCollect/HDRPrefix=HDR_');

}

var kUserCancelErr = 8007;

var autoCollect = new AutoCollector();

// These set up the handler  that verifies that the loaded event has been executed.
// This must be done before any select/deselect calls are made.
autoCollect.loadedHandler = function( event )
{
	if ((event.type == "loaded") && (event.location == "app"))
		autoCollect.loadedComplete = true;
	return { handled: false };	// Let default handlers also run
}

autoCollect.loadedEventHandler = { handler: autoCollect.loadedHandler };

autoCollect.addLoadedHandler = function()
{
	var i;
	for (i in app.eventHandlers)
		if (app.eventHandlers[i].handler == autoCollect.loadedHandler)
			return;
	app.eventHandlers.push( autoCollect.loadedEventHandler );
}

// CS4 used a clunky cache file to keep the folder structure in.  
// We Don't Need It Anymore
autoCollect.nukeOldXMLFile = function()
{
	var kCacheName = localize("$$$/JavaScripts/BridgeExtensions/AutoCollections/CacheName=AutoCollectionsCache.xml");
	var pathFolder = this.getDocumentPath();
	if (! pathFolder)
		return null;
	var xmlFile = new File( Folder(pathFolder) + "/" + kCacheName );
	if (xmlFile.exists)
		xmlFile.remove();
}

// Process the EXIF data into relevant information, and cache it in the
// thumbnail object.
autoCollect.cacheMetaData = function( thumb )
{
	if (! thumb.hasMetadata)
		return false;

	// Thumbnail must have EXIF metadata with DateTimeDigitized/Original before we can process it.
	var md = thumb.metadata;
	md.namespace = "http://ns.adobe.com/exif/1.0/";
	
	var exifTimeString = null;
	
	if ((typeof( md.DateTimeDigitized ) != "undefined")
		&& (md.DateTimeDigitized.length > 0))
		exifTimeString = md.DateTimeDigitized;
	else
		if ((typeof( md.DateTimeOriginal ) != "undefined")
			&& (md.DateTimeOriginal.length > 0))
			exifTimeString = md.DateTimeOriginal;
	
	if (! exifTimeString)
		return false;

	var i, m = exifTimeString.match(  /(\d{4})[:-](\d{2})[:-](\d{2})[T ](\d{2}):(\d{2}):(\d{2}).*/ );
	if (! m)	// Try again w/o seconds (Bridge 3.0 bug)
	{
		m = exifTimeString.match( /(\d{4})[:-](\d{2})[:-](\d{2})[T ](\d{2}):(\d{2}).*/)
		if (! m)
			return false;

		m[6] = 0;
	}

	for (i in m)
		m[i] = Number(m[i]);
	var timestamp = new Date( m[1], m[2], m[3], m[4], m[5], m[6] );

	thumb.cachedTime = timestamp.getTime() / 1000;

	function getValue( name, defaultValue, backup )
	{
		var mdval = md[name];
		if (mdval == "")
		{
			if (typeof(backup) != "undefined")
				mdval = md[backup];
		}
		if (mdval == "")
			return defaultValue;
		else
			return eval(mdval);
	}

	var aperture = getValue( "ApertureValue", 1.0, "FNumber" );
	var iso = getValue( "ISOSpeedRatings", 100 );
	var exposureTime = getValue( "ExposureTime", 0.125 );	// Could also look at ShutterSpeedValue if units were known
	var EV = 0.0;
	
	const sqrt2 = 1.414213562373095048801688724209698078570;
	
	EV = Math.log( iso ) / Math.log( 2.0 );
	EV += Math.log( exposureTime ) / Math.log( 2.0 );
	EV += (Math.log(64 * sqrt2 ) / Math.log( sqrt2 )) - aperture;
	thumb.cachedEV = EV;

	var flMatch;
	if ((typeof(md.FocalLength) != "undefined") && (flMatch = md.FocalLength.match(/(\d+[.]*\d*).*/)))
		thumb.cachedFL = eval(flMatch[1]);
	else
		thumb.cachedFL = 10.0;
		
	return true;
}

// Get the valid images from the current thumbnails & cache metadata
autoCollect.getDocumentImages = function()
{
	var i, kids = app.document.visibleThumbnails;
	var images = [];
	
	app.synchronousMode = true;	// Makes sure metadata is up to date
	
	// It might be nice to warn if images exist w/o DateTimeDigitized flags,
	// or if the list is empty.  
	for (i in kids)
		if (this.cacheMetaData(kids[i]))
			images.push(kids[i]);

	app.synchronousMode = false;

	// Sort first by mimeType, then by date; so simultaneous JPEG/RAW photos
	// aren't shuffled together.
	function sortFunction( a, b )
	{
		if (a.mimeType == b.mimeType)
			return a.cachedTime - b.cachedTime;
		else
			return (a.mimeType < b.mimeType) ? -1 : 1;
	}

	images.sort( sortFunction );
	return images;
}

// Find candidate groups of matching images based on
// meta-data alone.    AdobePatentID="B863"
autoCollect.findCandidateGroups = function( images )
{
	var i;

	this.candidateGroups = [];

	// Take a day off the base time so the times are all six digits (no "%0d" in JS)
	var baseTime = images[0].cachedTime - 100000;

	var curGroup = [images[0]];

	for (i = 0; i < images.length - 1; ++i)
	{
		if (this.matcher.matchMetadata( images[i], images[i+1] ))
		{
			curGroup[0].cachedCollectionType = this.matcher.collectionType;
			curGroup.push(images[i+1]);
		}
		else
		{
			if (curGroup.length > 1)
				this.candidateGroups.push( curGroup );
			curGroup = [images[i+1]];
		}
		if (curGroup.length > kMaxSameMetadata)
			throw("too many files");
	}

	if (curGroup.length > 1)
		this.candidateGroups.push( curGroup );
}

// If there's no cache, this takes care of finding the collections.
// Returns true if completed successfully.    AdobePatentID="B863"
autoCollect.createCollections = function( images, matcher )
{
	var i;
	
	this.matcher = matcher;

	this.findCandidateGroups( images );
	this.cur = 0;
	
	if (this.candidateGroups.length == 0)
		return;
	
	app.synchronousMode = true;
	
	// Use true for production, but if it bombs, you can't debug it with ESTK.
	// the false clause below skips the progress bar but is debuggable with ESTK.
	if (true)
	{
		this.progressWindow = new ProgressIndicator();
		this.progressWindow.workFunction = autoCollect.processCandidate;
		this.progressWindow.setMax( this.candidateGroups.length );
		this.progressWindow.start();
		if (this.progressWindow.isCancelled())
			throw kUserCancelErr;
	}
	else
	{
		for (i in this.candidateGroups)
			autoCollect.processCandidate( true );
	}
	app.synchronousMode = false;


	// Clean out any empty candidates after processing w/alignment code
	i = 0;
	while (i < this.candidateGroups.length)
	{
		if (this.candidateGroups[i].length == 0)
			this.candidateGroups.splice(i,1);
		else
			i++;
	}

	this.collectionGroups = this.collectionGroups.concat( this.candidateGroups );
	
	// On the Mac, multiple images in the same group causes a crash.
	// Avoid putting the same images in the next collection
	var j, numImages = images.length;
	i = 0;
	while (i < numImages)	// In JS 1.6, this would be one line of code ("filter")
	{
		j = 0;
		while ((i + j < numImages) && (images[i + j].cachedCollectionType != null))
			j++;
		if (j)
		{
			images.splice(i, j);
			numImages -= j;
		}
		else
			i++;
	}
}

// This handles each call to the alignmentLib, so it can
// run in the context of the progress bar.  Note we must
// explictly refer to "autoCollect" instead of "this", because "this"
// will be the progressWindow.     AdobePatentID="B863"
autoCollect.processCandidate = function( skipProgress )
{
	if (typeof(skipProgress) == "undefined")
		skipProgress = false;
//	$.writeln('Processing set ' + (autoCollect.cur+1) + ' of ' + autoCollect.candidateGroups.length);
	var j;
	var currentGroup = autoCollect.candidateGroups[autoCollect.cur];
	for (j in currentGroup)
	{
		c = currentGroup[j];
//		$.writeln( c.name + "   " + (c.cachedTime) + "   " + "  " + c.cachedFL + "  " + c.cachedEV );
	}
	var msg = localize('$$$/AdobeScript/BridgeExtension/ExamImages=Examining images...');
	if (! skipProgress)
		autoCollect.progressWindow.fileMessage( msg, currentGroup );

	try {
		var alignInfo = alignmentLib.getAlignmentInfo(currentGroup);
		
		// If there are multiple groups in the alignment, split them off and process them separately.
		if (alignInfo.groupCount.length > 1)
		{
			var remainder = currentGroup.splice( alignInfo.groupCount[0], currentGroup.length - alignInfo.groupCount[0] );
			var k;
			for (k = 1; k < alignInfo.groupCount.length; k++)
			{
				var newGroup = remainder.splice( 0, alignInfo.groupCount[k] );
				if (newGroup.length > 1)
				{
					autoCollect.candidateGroups.push( newGroup );
					if (! skipProgress)
						this.window.progressBar.maxvalue++;
				}
				else
					newGroup[0].cachedCollectionType = null;	// Mark available again
			}
		}
		autoCollect.matcher.scrubAlignedImages(alignInfo, currentGroup);
	}
	catch (error)
	{
		// Must close the progress indicator in this context
		autoCollect.progressWindow.window.close();
		// Flag progressIndicator to stop working
		autoCollect.cur = autoCollect.candidateGroups.length;
	}

	autoCollect.cur++;
}

// Deal with the screwy URL the "path" might be if show subfolders is on
autoCollect.getDocumentPath = function()
{
	var i, queryStr = "bridge:special";
	var targetStr = "target=bridge:fs:file:";
	var result = null;
	
	function startsWith( a, b )
	{
		return ( a.slice(0, b.length) == b );
	}
	
	if (startsWith( app.document.thumbnail.path, queryStr))
	{
		strs = app.document.thumbnail.path.split('&');
		for (i in strs)
			if (startsWith( strs[i], targetStr ))
				result = decodeURI( strs[i] ).slice( targetStr.length );
		// Blows up here if result not found
		return result ? result.slice( File.fs == "Windows" ? 3 : 2 ) : null;	// Remove extra /'s
	}
	else
		return app.document.thumbnail.path;	
}

// Check to see if the properties in the stacks are valid
// If the mod timestamp of the folder is newer than the timestamp
// of when the stack was made, then it's invalid.
autoCollect.checkValidStacks = function()
{
	var folderModDate = Folder(this.getDocumentPath()).modified;
	var stacks = app.document.stacks;
	var ourStacksFound = false;
	
	for (s in stacks)
	{
		if (stacks[s].isValid() && stacks[s].properties[kTimeAttr])
		{
			ourStacksFound = true;
			var d = new Date( stacks[s].properties[kTimeAttr] );
			if (d < folderModDate)
				return false;
		}
	}

	return ourStacksFound;
}

// Reload the collection information from the properties in the
// stacks
autoCollect.loadCollectionFromStackProps = function()
{
	this.collectionGroups = [];
	var stacks = app.document.stacks;
	
	for (i in stacks)
		if (stacks[i].properties[kTypeAttr] && stacks[i].isValid())
		{
			var group = [];
			for (j in stacks[i].thumbnails)
				group.push( stacks[i].thumbnails[j] );
				
			group[0].cachedCollectionType = stacks[i].properties[kTypeAttr];
			this.collectionGroups.push( group );
		}

	return true;
}

// Remove all the stacks in the document (sorry if you'd made some by hand...)
autoCollect.unstackDocument = function()
{
	// Unstack
	app.document.selectAll();
	app.document.chooseMenuItem("StackUngroup");
	app.document.deselectAll();
	app.document.refresh();
	
	// Nuke the record of the stack properties
	app.document.flushStackProperties();
	$.sleep(300);
}

// Remove only stacks created by the autoCollect code
// More tweakage: Could save stacks adjusted by user?
autoCollect.removeAutoStacks = function()
{
	var s, stacks = app.document.stacks;
	
	function removeStack( stack )
	{
		var i;
		for (i in stack.thumbnails)
			app.document.select( stack.thumbnails[i] );
		app.document.chooseMenuItem("StackUngroup");
		app.document.deselectAll();
	}

	app.document.deselectAll();

	for (s in stacks)
		if (stacks[s].properties[kTypeAttr])
			removeStack( stacks[s] )
	
	app.document.refresh();
	$.sleep(300);
	app.document.flushStackProperties();
}

// Load the collections, either from the stack's property caches or (if no good)
// by examining the images in this folder and rebuilding the stacks
autoCollect.stackCollections = function()
{
	function gatherStack( thumbGroup )
	{
		if (thumbGroup.length > 1)
		{
			app.document.deselectAll();
			for (j in thumbGroup)
				app.document.select( thumbGroup[j] )
			app.document.chooseMenuItem("StackGroup");
			lastStack = app.document.stacks[app.document.stacks.length-1];
			lastStack.properties[kTimeAttr] = Date().toString();
			lastStack.properties[kTypeAttr] = thumbGroup[0].cachedCollectionType;
		}
	}

	if ((!this.checkValidStacks()) || (!this.loadCollectionFromStackProps()))
	{
		this.nukeOldXMLFile();
		
		this.removeAutoStacks();

		var images = this.getDocumentImages();
		
		if (images.length == 0)	// Bail now if there's nothing to process
			throw("no_timestamps");
		
		this.collectionGroups = [];

		this.createCollections( images, panoramaMatcher );
		
		// Bail out now if we couldn't load alignmentLib
		if (alignmentLib.loadLibraryFailed)
		{
			this.collectionGroups = [];
			return;
		}
		if (images.length > 2)
			this.createCollections( images, HDRMatcher );

		var i;
		for (i in this.collectionGroups)
			gatherStack( this.collectionGroups[i] );
		app.document.flushStackProperties();
		app.document.deselectAll();
		$.sleep(300);
		app.document.refresh();
	}
	if (this.collectionGroups.length == 0)
		alert( localize( '$$$/BridgeExtension/AutoCollect/NoCollectionsFound=No panorama or HDR image sets found.' ));
}

// Hand off the collections to Photoshop.  Note: this returns long
// before Photoshop is done crunching.
autoCollect.processCollectionsInPhotoshop = function()
{
	// Create the stacks, if they don't exist already.
	this.stackCollections();
	
	var i, j;
	this.cur = 0;
	
	// Use true for production, but if it bombs, you can't debug it with ESTK.
	// the false clause below skips the progress bar but is debuggable with ESTK.
	var useProgressIndicator = true;

	if (useProgressIndicator)
	{
		this.progressWindow = new ProgressIndicator();
		this.progressWindow.workFunction = autoCollect.processCollectionInPS;
		this.progressWindow.setMax( this.collectionGroups.length );
		this.progressWindow.start();
		if (this.progressWindow.isCancelled())
			throw kUserCancelErr;
	}
	else
	{
		for (i in this.collectionGroups)
			this.processCollectionInPS( true );
	}
	app.bringToFront();
	
	if (useProgressIndicator)
		this.progressWindow.window.close();
		
	// Go over the stacks and reset their mod times to allow for the new
	// files we've created
	for (i in app.document.stacks)
		if (app.document.stacks[i].properties[kTypeAttr])
			app.document.stacks[i].properties[kTimeAttr] = Date().toString();
	app.document.flushStackProperties();
}

// This is the "work" function called by the progress indicator
autoCollect.processCollectionInPS = function( skipProgress )
{
	// Make a range from camera index numbers, if we can.
	function getNumberString( group )
	{
		var n1 = group[0].name;
		var n2 = group[group.length-1].name;
		// Assume the index is just in front of the .XXX suffix
		n1 = n1.match(/[^\d]*(\d+)[.]\w+$/);
		n2 = n2.match(/[^\d]*(\d+)[.]\w+$/);
		if ((n1 && n2) && (n1[1] != n2[1]))
			return n1[1] + "_" + n2[1];
		else
			return null;
	}

	// If we don't have image serial numbers
	// to work with, attempt to conjure something
	// remotely plausible.
	function getCollectionName( group )
	{
		function removeSuffix(s)
		{
			var ns = s.match(/(.*)[.][\w]+$/);
			return ns ? ns[1] : s;
		}
				
		var n1 = removeSuffix(group[0].name);
		var n2 = removeSuffix(group[group.length-1].name);
		var i, noneEqual = true;
		var left = "", right = "";
		for (i = 0; i < Math.min(n1.length, n2.length); ++i)
			if (n1[i] != n2[i])
			{
				left += n1[i];
				right += n2[i];
			}
			else
				noneEqual = false;
				
		if (noneEqual)
			return n1 + "_" + n2;
		return left + "_" + right;
	}
	
	if (typeof(skipProgress) == "undefined")
		skipProgress = false;
	
	var basePath = decodeURI(Folder(autoCollect.getDocumentPath()).toString() + "/");
	var g = autoCollect.collectionGroups[autoCollect.cur];
	var numString = getNumberString( g );
	if (! numString)
		numString = getCollectionName( g );
	var files = [];
	var dest = basePath + ((g[0].cachedCollectionType == kHDRKey) ? autoCollect.HDRPrefix : autoCollect.panoPrefix);
	dest += numString;
	
	// Make sure we don't clobber an existing file
	if (File(dest + ".psd").exists)
	{
		var dupe_index = 1;
		while (File(dest + "_" + dupe_index + ".psd").exists)
			dupe_index++;
		dest += ("_" + dupe_index);
	}
	dest += ".psd";
	files.push(encodeURI(dest));
	var i, msgFiles = [];
	for (i in g)
	{
		files.push(encodeURI(g[i].path));
		msgFiles.push(g[i]);
	}

	var msg = localize('$$$/AdobeScript/BridgeExtension/PSProcImages=Processing in Photoshop...');

	if (! skipProgress)
		autoCollect.progressWindow.fileMessage( msg, msgFiles );

	if (g[0].cachedCollectionType == kHDRKey)
		photoshop.noDialogMergeToHDR( files );
	else
		photoshop.noDialogPhotomerge( files );
		
	// Wait until Photoshop actually starts processing the request...
	while (BridgeTalk.getStatus("photoshop") != "PUMPING")
	{
		if (! skipProgress)
			autoCollect.progressWindow.window.update();	// For Mac - keep SUI event Queue alive
		$.sleep(100);
	}

	// ...then don't move on until PS has finished processing.
	while (BridgeTalk.getStatus("photoshop") != "IDLE")
	{
		if (! skipProgress)
			autoCollect.progressWindow.window.update();
		$.sleep(500);
	}

	autoCollect.cur++;
}

autoCollect.loadAndDoCollection = function( postLoadFunction )
{
	try {
		eval( "autoCollect." + postLoadFunction + "();" );
		$.sleep(300);
		app.document.refresh();
	}
	catch (error)
	{
		if (error == "no_timestamps")
			alert(localize('$$$/BridgeExtensions/AutoCollect/NoTimestamps=No timestamps were found on any of the image files'));
		else
		if (error == "too many files")
			alert(localize('$$$/BridgeExtensions/AutoCollect/TooManyFiles=Too many files were found with matching metadata.'));
		else
		if (error != kUserCancelErr)
			alert(localize('$$$/BridgeExtensions/AutoCollect/Unknownexception=Unknown Exception'));
	}
}

//==============================================================================

// Avoid duplicating the menus
if (typeof(autoCollectMenu) == "undefined")
	autoCollectMenu = new MenuElement( 'command', localize('$$$/BridgeExtensions/AutoCollect/AutoCollectMenu=Auto-Stack Panorama/HDR'), 'at the end of submenu/Stack');

autoCollectMenu.onSelect = function(m)
{
	autoCollect.loadAndDoCollection( "stackCollections" );
}

if (typeof(PSCollectMenu) == "undefined")
	PSCollectMenu = new MenuElement('command', localize('$$$/BridgeExtensions/AutoCollect/AutoCollectPS=Process Collections in Photoshop'), 'at the end of tools/ps');

PSCollectMenu.onSelect = function(m)
{
	autoCollect.loadAndDoCollection( "processCollectionsInPhotoshop" );
}

// Uncomment to debug in ESTK
function testAS()
{
 then = new Date();
 autoCollect.loadAndDoCollection( "stackCollections" );
 now = new Date();  $.writeln('time: ' + (now - then) / 1000.0 );
}

// Uncomment to debug in ESTK
function testASPS()
{
 then = new Date();
 autoCollect.loadAndDoCollection( "processCollectionsInPhotoshop" );
 now = new Date();  $.writeln('time: ' + (now - then) / 1000.0 );
}

// Quickly pretend it never happened...
function resetAS()
{
	autoCollect.unstackDocument();
	app.document.flushStackProperties();

	// Delete pano/HDR files created by Photoshop
	var i, folder = Folder(autoCollect.getDocumentPath());
	var files = folder.getFiles('Pano_*.psd');
	files = files.concat(folder.getFiles('HDR_*.psd'));
	for (i in files)
		files[i].remove();
}

function dumpStacks()
{
	var i, j, stacks = app.document.stacks;
	
	function ifWriteln(msg, data)
	{
		if (data)
			$.writeln( msg + data );
	}
	
	for (i in stacks)
	{
		$.writeln('------------');
		for (j in stacks[i].thumbnails)
			$.write( stacks[i].thumbnails[j].name + ' ');
		$.writeln();
		ifWriteln('Timestamp: ', stacks[i].properties[kTimeAttr]);
		ifWriteln('Type: ', stacks[i].properties[kTypeAttr]);
		$.writeln('Stack is ' + stacks[i].isValid() ? "Valid" : "Not valid");
	}
}		
