Source of “photos.js”
// Emacs settings: -*- mode: Fundamental; tab-width: 4; -*-

////////////////////////////////////////////////////////////////////////////
//                                                                        //
// Andrew's Album Applications: photos.js                                 //
//                                                                        //
// Uses functions from common.js and crypto.js                            //
//                                                                        //
// Copyright (c) 2004-2005, Andrew Birrell                                //
//                                                                        //
////////////////////////////////////////////////////////////////////////////


//
// Global variables
//

var wSize;                  // window size
var mainSize = Object();    // size of main image display area
var iPhone = false;         // fine tuning of the UI

var dropZone;               // dropzone element
var reading;                // page element to show while reading a page
var scaling;                // page element to show current scale
var title;                  // title element
var parents;                // element for parent links
var parentsTxt;             // HTML for top-level parent link
var writing;                // element for "writing" message
var editInner;              // inside of edit dialog
var editTitle;              // edit dialog title type-in element
var linkAnchor;             // <A> in "Link" button

var btns = new Object();    // button details
var dlogs = new Object();   // pop-up dialogs
var dlogPositioned = new Object();

var thisPage = null;        // attributes for current page

var user = \\\\"\\\\";          // default user for editing
var pwd = \\\\"\\\\";       // default password for editing
var jumpTarget = null;      // if set, switch to here instead
var timer = null;           // auto-play timer, for cancelling
var autoRoot = null;        // root directory for auto-play
var preloadServer = false;  // variant of auto-play to preload server cache
var interval = 6000;        // auto-play timer interval
var timerScaleOn = null;    // timer for showing "scaling" dlog
var timerScaleOff = null;   // timer for hiding "scaling" dlog

var cacheP = new Cache(50); // cache of photo XML responses
var cacheF = new Cache(50); // cache of folder XML responses
var cacheHits = 0;
var cacheMisses = 0;

var parentFrag =            // Template HTML fragment for parent link
    '<a href="#<#PATH>" onclick="' +
        "return doParent('<#PATH>')" +
        '" title="Go back to &quot;<#TITLE>&quot;"><#TITLE></a>';
var folderFrag =            // Template HTML fragment for folder listing
    '<td class=mini><a href="#<#PATH>" onclick="' +
        "return jumpTo('<#PATH>')" +
        '" title="Go to &quot;<#TITLE>&quot;">' +
        '<img class=mini width="<#WIDTH>" height="<#HEIGHT>" ' +
        'src="<#SRC>"></a></td>' +
    '<td class=miniTitle><a href="#<#PATH>" onclick="' +
        "return jumpTo('<#PATH>')" +
        '" title="Go to &quot;<#TITLE>&quot;"><#TITLE></a></td>';
var photoFrag =             // Template HTML fragment for photo listing
    '<a href="#<#PATH>" title="<#TITLE>" onclick="' +
        "return jumpTo('<#PATH>')" +
        '"><img class=thumb width="<#WIDTH>" height="<#HEIGHT>" ' +
        'src="<#SRC>" style="margin-left: <#MARGINL>px; ' +
        'margin-right: <#MARGINR>px; margin-top: <#MARGINT>px; ' +
        'margin-bottom: <#MARGINB>px"></a> ';
var mainFrag =              // Template HTML fragment for main screen
    '<a href="<#RAW>" target="_new"' +
//      ' title="Download the original full-scale image<#SIZE>"' +
        '><img id=<#ID> ' +
        'style="width: 1px; height: 1px" src="<#SRC>"></a>';
var scalingFrag =
    'Image&nbsp;area is now<br><#XPX> x <#YPX> pixels';


//
// Dragging
//

var dragObject = null;      // victim of the current drag, if any
var dragHighlight;          // element whose class is changed during drag
var dragStartM = Object();  // initial mouse coordinates
var dragStartO;             // initial object coordinates, doc relative
var dragSaveClass;          // initial class of dragHighlight
var dragConstraint;

function dragStart(event, id, highlightId, highlightClass,
        constraint) {
    // Called on mousedown on the drag handle.
    // Sets things up to drag the element named by "id".
    // If "highlightId" is present (non-null), it's the name of an element
    // whose className should be changed to "highlightClass" during the
    // drag.
    dragObject = document.getElementById(id);
    if (dragObject) {
        dragStartO = getElementPos(dragObject);
        dragStartM.x = event.clientX;
        dragStartM.y = event.clientY;
        document.onmousemove = dragMove;
        document.onmouseup = dragStop;
        document.onmouseout = dragOut;
        if (highlightId) {
            dragHighlight = document.getElementById(highlightId);
        } else {
            dragHighlight = null;
        }
        if (dragHighlight) {
            dragSaveClass = dragHighlight.className;
            dragHighlight.className = highlightClass;
        }
    }
    dragConstraint = constraint;
    return false;
}

function dragMove(event) {
    // Called on mouse movement (in IE, only if it happens in our window)
    // Perform the drag
    if (dragObject) {
        if (!event) event = window.event; // IE versus the rest
        var lMin = (dragConstraint ? dragConstraint.lMin : 0);
        var lMax = (dragConstraint ? dragConstraint.lMax : 999999);
        var tMin = (dragConstraint ? dragConstraint.tMin : 0);
        var tMax = (dragConstraint ? dragConstraint.tMax : 999999);
        var newLeft = Math.max(lMin, Math.min(lMax,
            dragStartO.x + event.clientX - dragStartM.x));
        var newTop = Math.max(tMin, Math.min(tMax,
            dragStartO.y + event.clientY - dragStartM.y));
        dragObject.style.left = "" + newLeft + "px";
        dragObject.style.right = "auto";
        dragObject.style.top = "" + newTop + "px";
        dragObject.style.bottom = "auto";
        if (dragConstraint && dragConstraint.draggedTo) {
            dragConstraint.draggedTo(newLeft, newTop);
        }
    }
    return false;
}

function dragStop(event) {
    // Called on mouse up (in IE, only if it happens in our window)
    if (dragObject) {
        if (!event) event = window.event; // IE versus the rest
        if (dragHighlight) dragHighlight.className = dragSaveClass;
        dragObject = null;
        document.onmousemove = null;
        document.onmouseup = null;
        document.onmouseout = null;
        if (dragConstraint && dragConstraint.dragEnded) {
            dragConstraint.dragEnded();
        }
    }
    return false;
}

function dragOut(event) {
    // Called on mouse leaving its target element
    // We want to terminate the drag if the mouse leaves the window
    // (it can transiently leave dragObject during fast dragging)
    if (dragObject) {
        if (!event) event = window.event; // IE versus the rest
        if (!event.relatedTarget && !event.toElement) {
            // It didn't go anywhere, so it must have left completely
            dragStop(event);
        }
    }
    return false;
}


//
// Button management
//

function Button(id, action, photoTip, folderTip) {
    // Constructor for a Button object
    this.element = document.getElementById(id);
    if (this.element) {
        this.txt = this.element.innerHTML;
        this.photoHTML = '<a href="./" onclick="return ' +
            action + '" title="' + photoTip + '">' + this.txt + '</a>';
        this.folderHTML = '<a href="./" onclick="return ' +
            action + '" title="' + folderTip + '">' + this.txt + '</a>';
    }
    this.disable();
}

Button.prototype.enable = function() {
    // Enable a button
    if (this.element) {
        this.element.innerHTML = 
            (thisPage.isPhoto ? this.photoHTML : this.folderHTML);
    }
    this.enabled = true;
    this.visible = true;
}

Button.prototype.disable = function() {
    // Disable a button
    if (this.element) {
        this.element.innerHTML =
            '<span class=disabled>' + this.txt + '</span>';
    }
    this.enabled = false;
    this.visible = true;
}

Button.prototype.hide = function() {
    // Remove button from the screen
    if (this.element) this.element.innerHTML = "";
    this.enabled = false;
    this.visible = false;
}


//
// Progress bar animation and "reading" dialog
//
// This is purely for entertainment: it doesn't actually measure progress.
// It assumes that the bar widget should be sized within its parent.
//

var progressBarWidget = null;   // the widget being scaled
                                // maximum width is the widget's parent
var progressBarTimer = null;    // timer for reading progress bar
var progressBarW = 0;           // reading progress bar state

function stepProgressBar() {
    // Animate the readProgress widget
    var maxWidth = progressBarWidget.parentNode.offsetWidth;
    progressBarW += (progressBarW <= 20 ? 4 :
        (progressBarW <= 40 ? 3 : 
            (progressBarW <= 60 ? 2 : 1)));
    if (progressBarW > maxWidth-4) progressBarW = 0;
    progressBarWidget.style.width = "" + progressBarW + "px";
}

function startProgressBar(widget) {
    // Start a progress bar using the element named "widget"
        progressBarWidget = document.getElementById(widget);
        progressBarWidget.style.width = "4px"; // amount initially visible
        progressBarW = 4;
        progressBarTimer = setInterval(stepProgressBar, 500);
}

function stopProgressBar() {
    // Stop animating the progress bar
    if (progressBarTimer) clearInterval(progressBarTimer);
    progressBarTimer = null;
}

function noteReading() {
    // Show the "reading" dialog and disable buttons as appropriate
    if (reading.style.display != 'block') {
        reading.style.display = "block";
        moveToCenter(reading, wSize);
        startProgressBar("readingWidget");
        btns.next.disable();
        btns.prev.disable();
        btns.skip.disable();
        if (btns.auto.visible) btns.auto.disable();
        btns.edit.disable();
        if (dlogs.editForm.style.display == 'block') {
            editTitle.blur();
            editInner.style.visibility = "hidden";
        }
    }
}

function endOfReading() {
    // enable/disable buttons as appropriate for current page,
    // and dispense with the "reading" dialog
    stopProgressBar();
    reading.style.display = "none";
    if (preloadServer) {
        btns.next.disable();
        btns.prev.disable();
        btns.skip.disable();
        btns.edit.disable();
    } else {
        if (thisPage.nextImage != "" || thisPage.isPhoto) {
            btns.next.enable();
        } else {
            btns.next.disable();
        }
        if (thisPage.prevImage != "" || thisPage.isPhoto) {
            btns.prev.enable();
        } else {
            btns.prev.disable();
        }
        if (thisPage.skipImage != "") {
            btns.skip.enable();
        } else {
            btns.skip.disable();
        }
        btns.edit.enable();
    }
    if (timer) {
        btns.auto.hide();
        btns.pause.enable();
        btns.resume.hide();
        btns.stop.enable();
    } else if (autoRoot) {
        btns.auto.hide();
        btns.pause.hide();
        btns.resume.enable();
        btns.stop.enable();
    } else {
        if (thisPage.autoImage != "") {
            btns.auto.enable();
        } else {
            btns.auto.disable();
        }
        btns.pause.hide();
        btns.resume.hide();
        btns.stop.disable();
    }
}


//
// Scaling the main image
//

function computeMainSize() {
    // Compute appropriate values for current size of "td.main" element
    // and adjust it in the DOM.
    // NOTE: in Safari 1.3.1, td.main is at the top of the image, which
    // can be below the top of the main table.  (In IE 6, both return the
    // same top value.)
    var mainTable = document.getElementById("mainTable");
    var main = document.getElementById("main");
    var bottom = document.getElementById("bottomWrapper");
    var mainPos = getElementPos(mainTable);
    wSize = windowSize();
    var h = wSize.y - mainPos.y - bottom.offsetHeight;
    main.style.height = "" + h + "px";
    mainSize.x = wSize.x;
    mainSize.y = h;
}

function scaleMainImage() {
    // Make the main image, if any, fit the "td.main" element
    // Used for onresize events, and when loading the page
    computeMainSize();
    var mainImage = document.getElementById(thisPage.path);
    if (mainImage) {
        if (thisPage.isPhoto) {
            var xScale = mainSize.x / thisPage.photoActualW;
            var yScale = mainSize.y / thisPage.photoActualH;
            // iPhone uses a viewscreen larger than the physicaly screen.
            // So images will be scaled down again, so we allow a scale-up.
            var scale = Math.min((iPhone ? 2 : 1),
                            Math.min(xScale, yScale));
            mainImage.style.width = "" +
                Math.round(thisPage.photoActualW * scale) + "px";
            mainImage.style.height = "" +
                Math.round(thisPage.photoActualH * scale) + "px";
        } else {
            // iPhone's gesture for scrolling a fixed-size inner DIV is
            // awkward, and unknown by the vast majority of users.  So on an
            // iPhone we make the inner DIV height "auto".  However, td.main
            // has a fixed size too: if mainImage is less than that, the
            // size from td.main applies, placing the bottom menu bar at
            // the bottom of the screen; if mainImage is more than that,
            // "auto" makes it expand td.main.
            if (iPhone) {
                mainImage.style.height = "auto";
            } else {
                mainImage.style.height = "" + mainSize.y + "px";
            }
        }
    }
}

function scaleOn() {
    timerScaleOn = null;
    if (!iPhone) {
        // Don't show scale on iPhone ... it happens too often, and isn't
        // readable anyway.
        scaling.style.display = 'block';
        var temp = scalingFrag.replace(/<#XPX>/, mainSize.x);
        temp = temp.replace(/<#YPX>/, mainSize.y);
        scaling.innerHTML = temp;
        moveToCenter(scaling, wSize);
        if (timerScaleOff) clearTimeout(timerScaleOff);
        timerScaleOff = window.setTimeout(scaleOff, 1000);
    }
}

function scaleOff() {
    if (scaling.style.display == 'block') scaling.style.display = 'none';
}

function doResize() {
    // Respond to window resizing
    scaleOff();
    if (thisPage) {
        scaleMainImage();
        // Showing the "scaling" window during resize causes significant
        // flicker in IE, so we defer it until dragging stops
        if (timerScaleOn) clearTimeout(timerScaleOn);
        timerScaleOn = window.setTimeout(scaleOn, 100);
    }
}


//
// Prefetching Images
//

// We could get images pre-loaded by placing them all into some HTML
// and handing that to the browser.  But then there'd be no way
// to cancel the load requests if we want to go somewhere else.
// By doing them sequentially ourselves, we get that control.
//
// This doesn't work on iPhone (v.2).

var imgQueue = null;                // queue of images to preload
var imgQueueTail = null;

function initiateImageLoad() {
    if (imgQueue) {
        var prefetch = document.getElementById("prefetch");
        prefetch.onload = stepImagePreload;
        prefetch.src = imgQueue.src;
    }
}

function  stepImagePreload() {
    // Current preload has completed; initiate next
    imgQueue = imgQueue.next;
    if (imgQueue) {
        // execute next asynchronously, to avoid recursive overflow
        setTimeout(initiateImageLoad, 10);
    }
}

function enqueueImage(src) {
    // enqueue an image for preloading
    if (!iPhone) {
        var queue = new Object();
        queue.src = src;
        queue.next = null;
        if (imgQueue) {
            imgQueueTail.next = queue;
        } else {
            imgQueue = queue;
            initiateImageLoad();
        }
        imgQueueTail = queue;
    }
}

function cancelImagePreload() {
    // Dispense with the image preload queue
    if (imgQueue) imgQueue.next = null;
}


//
// Page interpretation
//

function getTitle(node) {
    // Return the value of the "title" child of given XML node
    var titles = node.getElementsByTagName("title");
    var child = titles[0].firstChild;
    // The title is the child, but there's no child if the title was empty
    return (child ? child.nodeValue : "");
}

function buildPage(responseXML) {
    // Construct and return a page object based on the given XML
    var doc = responseXML.documentElement;
    newPage = new Object();
    newPage.responseXML = responseXML;
    newPage.path = doc.getAttribute("path");
    newPage.title = getTitle(doc);
    newPage.nextImage = doc.getAttribute("next");
    newPage.prevImage = doc.getAttribute("prev");
    newPage.skipImage = doc.getAttribute("skip");
    var starLevel = doc.getAttribute("starLevel");
    newPage.starLevel = (starLevel ? parseInt(starLevel) : null);
    newPage.parents = ''; // parent links HTML
    var i;
    var parents = doc.getElementsByTagName("parent");
    newPage.lastParent = "";
    for (i = 0; i < parents.length; i++) {
        var child = parents[i];
        var chtitle = getTitle(child);
        var chpath = child.getAttribute("path");
        if (i == 0) {
            chtitle = parentsTxt;
        } else {
            newPage.parents += '&nbsp; &gt;&nbsp; ';
        }
        var temp = parentFrag.replace(/<#PATH>/g, chpath);
        temp = temp.replace(/<#TITLE>/g, htmlspecials(chtitle));
        newPage.parents += temp;
        newPage.lastParent = chpath;
        newPage.autoRoot = chpath; // use last parent for photos
    }
    if (doc.nodeName == "photo") {
        newPage.isPhoto = true;
        newPage.autoImage = // enable auto iff next is in this folder
            (newPage.autoRoot == "." ||
                newPage.nextImage.indexOf(newPage.autoRoot) == 0 ?
                    newPage.nextImage : "");
        var width = parseInt(doc.getAttribute("width"));
        var height = parseInt(doc.getAttribute("height"));
        var src = doc.getAttribute("src");
        var raw = doc.getAttribute("raw");
        var size = doc.getAttribute("size");
        var date = doc.getAttribute("date");
        var exposure = doc.getAttribute("exposure");
        newPage.photoActualW = width;
        newPage.photoActualH = height;
        if (preloadServer) {
            newPage.html = "<br><br><br><br><br><br><br><br>" +
                "Preloading the server-side cache," +
                " with no image display and no delays.<p>" +
                "Click \"Stop\" to exit from this mode.";
        } else {
            var temp = mainFrag.replace(/<#SRC>/, src);
            temp = temp.replace(/<#RAW>/, raw);
            temp = temp.replace(/<#ID>/, newPage.path);
            temp = temp.replace(/<#SIZE>/, "\n" + size);
            newPage.html = temp;
        }
        newPage.commentary = (date ? htmlspecials(date) : "&nbsp;") +
            "<br>" +
            (exposure ? htmlspecials(exposure) : "&nbsp;")
    } else {
        // folder
        newPage.isPhoto = false;
        if (parents.length > 0) {
            newPage.parents += '&nbsp; &gt;&nbsp; ';
        }
        newPage.parents += '<span class=disabled>' +
            (parents.length == 0 ? parentsTxt :
                        htmlspecials(newPage.title)) +
            '</span>';
        newPage.autoImage = doc.getAttribute("first");
        newPage.autoRoot = newPage.path; // use self for folders
        content = '<div class=catalog id="' + newPage.path + '">';
        var children = doc.getElementsByTagName("folder");
        var folderCount = children.length;
        var lCol = 0; // child index for left column
        var rCol = Math.round((folderCount+0.5)/2);
        var i;
        for (i = 0; i < folderCount; i++) {
            var sub;
            if (i%2 == 0) {
                content += (i == 0 ?
                    "<table class=catalog cellspacing=0>" :
                    "</tr>");
                content += "<tr>";
                sub = children[lCol]; lCol++;
            } else {
                sub = children[rCol]; rCol++;
            }
            var chpath = sub.getAttribute("path");
            var chtitle = getTitle(sub);
            var width = sub.getAttribute("width");
            var height = sub.getAttribute("height");
            var src = sub.getAttribute("src");
            var temp = folderFrag.replace(/<#PATH>/g, chpath);
            temp = temp.replace(/<#WIDTH>/, width);
            temp = temp.replace(/<#HEIGHT>/, height);
            temp = temp.replace(/<#SRC>/, src);
            temp = temp.replace(/<#TITLE>/g, htmlspecials(chtitle));
            content += temp;
        }
        if (i != 0) content += "</tr></table>";
        var photoMaxW = parseInt(doc.getAttribute("photoMaxW"));
        children = doc.getElementsByTagName("photo");
        var photoCount = children.length;
        for (i = 0; i < photoCount; i++) {
            var sub = children[i];
            var chpath = sub.getAttribute("path");
            var chtitle = getTitle(sub);
            var width = parseInt(sub.getAttribute("width"));
            var height = parseInt(sub.getAttribute("height"));
            var src = sub.getAttribute("src");
            var temp = photoFrag.replace(/<#PATH>/g, chpath);
            temp = temp.replace(/<#TITLE>/g, htmlspecials(chtitle));
            temp = temp.replace(/<#WIDTH>/, width);
            temp = temp.replace(/<#HEIGHT>/, height);
            temp = temp.replace(/<#SRC>/, src);
            var lMargin = 6 + Math.floor((photoMaxW-width)/2);
            // Vertically, we can just use a fixed margin, because our style
            // lays the images out with "vertical-align: center".  This
            // way rows are closer together when there are no portrait-mode
            // photos.
            temp = temp.replace(/<#MARGINL>/, lMargin);
            temp = temp.replace(/<#MARGINR>/, 6+photoMaxW-(width+lMargin));
            temp = temp.replace(/<#MARGINT>/, 4);
            temp = temp.replace(/<#MARGINB>/, 0);
            content += temp;
        }
        newPage.commentary =
            (folderCount > 0 ? "" + folderCount + " folders" : "&nbsp;") +
            (folderCount > 0 && photoCount > 0 ? " and" : "") +
            "<br>" +
            (photoCount > 0 ? "" + photoCount + " photographs" : "&nbsp;")
        newPage.html = content + '<br>&nbsp;</div>';
    }
    return newPage;
}

function displayPage() {
    // Update the display based on the current page attributes
    setCookie("AndrewAlbumApp", thisPage.path);
    var commentary = document.getElementById("commentary");
    if (thisPage.commentary) {
        commentary.innerHTML = thisPage.commentary;
    } else {
        commentary.innerHTML = "&nbsp;<br>&nbsp;";
    }
    dropZone.innerHTML = thisPage.html;
    window.scrollTo(0,0);
    title.innerHTML = htmlspecials(thisPage.title);
    parents.innerHTML = thisPage.parents;
    showStarLevel();
    editTitle.value = thisPage.title;
    if (dlogs.editForm.style.display == 'block') {
        editInner.style.visibility = "visible";
        editTitle.focus();
        editTitle.select();
    }
    scaleMainImage();
    linkAnchor.href = '#' + thisPage.path;
    endOfReading();
}


//
// Page access
//

var fetching = new Object(); // paths currently being fetched from server

function getXML(path, jumpTo) {
    // Fetch XML for given path.  If it's already in cache, return the XML;
    // if it's already being fetched, do nothihng, otherwise initiate fetch
    // "path" was urlencoded by the server-side script.
    // Iff "jumpTo", arrange to display the page when it arrives.
    var cached = cacheP.read(path);
    if (!cached) cached = cacheF.read(path);
    if (cached) {
        if (jumpTo) jumpTarget = null; // cancel any in-progress jumpTarget
        return cached;
    }
    if (jumpTo) {
        jumpTarget = path;
        noteReading();
    }
    if (!fetching[path]) {
        var thisReq = new Object();
        thisReq.requestedPath = path;
        fetching[path] = thisReq;
        get(thisReq, "photos.php?op=xml&path=" + path +
            (iPhone ? "&pda=" : ""));
    }
}

function loadPage(responseXML) {
    // Load the given page's responseXML into the window.
    // Also initiate page prefetching, and refresh auto-display timer
    thisPage = buildPage(responseXML);
    cancelImagePreload();
    fetching = new Object(); // abandon previous page prefetching
    displayPage();
    if (thisPage.path == autoRoot) manual();
    if (!preloadServer) {
        // pre-fetch nearby pages
        if (thisPage.autoImage != "") getXML(thisPage.autoImage, false);
        if (thisPage.nextImage != "") getXML(thisPage.nextImage, false);
        if (thisPage.lastParent != "") getXML(thisPage.lastParent, false);
    }
    if (timer) timer =
        window.setTimeout(doNext, (preloadServer ? 10 : interval));
}

function handleFailure(thisReq) {
    // Completion procedure called on failures
    // Redisplay the current page (undo "showReading")
    cancelImagePreload();
    fetching = new Object();
    if (thisPage) displayPage();
}

function handleResult(thisReq) {
    // Completion procedure: interpret the result, assumed to be XML
    var responseXML = thisReq.xmlhttp.responseXML;
    if (!responseXML) alert(thisReq.xmlhttp.responseText);
    var doc = responseXML.documentElement;
    if (doc.nodeName == "save") {
        // Result of editSave
        var status = doc.getAttribute("status");
        var path = doc.getAttribute("path");
        if (thisReq.isPhoto) {
            cacheP.flush(path);
        } else {
            cacheP = new Cache(50);  // parent names might have changed
            cacheF = new Cache(50);
        }
        editInner.style.visibility = "visible";
        writing.style.display = 'none';
        if (status == "ok") {
            if (editStayOpen) {
                doNext();
            } else {
                closeDlogs();
                jumpTo(thisPage.path);
            }
        } else {
            alert("Failed: " + status);
        }
    } else if (doc.nodeName == "photo" || doc.nodeName == "folder") {
        // Interpret the XML response for a folder or image page
        var path = doc.getAttribute("path");
        var thisCache = (doc.nodeName == "photo" ? cacheP : cacheF);
        thisCache.write(path, responseXML);
        if (jumpTarget == thisReq.requestedPath) {
            // Note that the server can return a page with a different
            // path, for example if the one we requested doesn't exist
            jumpTarget = null;
            loadPage(responseXML);
        } else if (fetching[path]) {
            fetching[path] = false;
            // enqueue preload requests for our images, if we're still OK
            if (preloadServer) {
                // don't preload any images
            } else if (doc.nodeName == "photo") {
                var src = doc.getAttribute("src");
                enqueueImage(src);
            } else { // folder
                var children = doc.getElementsByTagName("folder");
                for (var i = 0; i < children.length; i++) {
                    var sub = children[i];
                    var src = sub.getAttribute("src");
                    enqueueImage(src);
                }
                children = doc.getElementsByTagName("photo");
                for (var i = 0; i < children.length; i++) {
                    var sub = children[i];
                    var src = sub.getAttribute("src");
                    enqueueImage(src);
                }
            }
        }
    } else {
        alert('Unknown result, nodeName="' + doc.nodeName +'"');
    alert(thisReq.xmlhttp.responseText);
        handleFailure(thisReq);
    }
}

function jumpTo(path) {
    // Jump to given path
    if (timer) clearTimeout(timer);
    var cached = getXML(path, true);
    if (cached) {
        cacheHits++;
        loadPage(cached);
    } else {
        cacheMisses++;
    }
    return false;
}


//
// The top-level subroutines, mostly invoked from the HTML page
//

function containedJump(candidate) {
    // If candidate would move us outside of our folder (or autoRoot),
    // jump to the bounding folder; otherwise jump to the candidate
    var target = candidate;
    var container = (autoRoot ? autoRoot :
                        (thisPage.isPhoto ? thisPage.lastParent : null));
    if (container) {
        if (container == ".") {
            if (candidate == "") target = container;
        } else {
            if (candidate.indexOf(container) != 0) target = container;
        }
    }
    if (target != "") jumpTo(target);
}

function doNext() {
    // Perform the "next" operation.
    containedJump(thisPage.nextImage);
    return false;
}

function doPrev() {
    containedJump(thisPage.prevImage);
    return false;
}

function doSkip() {
    containedJump(thisPage.skipImage);
    return false;
}

function auto(event) {
    // start automatic mode
    autoRoot = thisPage.autoRoot;
    preloadServer = (event.shiftKey ? true : false);
    if (preloadServer) {
        // flush cache so that we actually use the server-side
        cacheP = new Cache(50);
        cacheF = new Cache(50);
    }
    if (!timer) timer = window.setTimeout("jumpTo(thisPage.autoImage)", 50);
    btns.auto.hide();
    btns.pause.enable();
    btns.resume.hide();
    btns.stop.enable();
    return false;
}

function pause() {
    // suspend automatic mode without losing track of root
    if (timer) clearTimeout(timer);
    timer = null;
    btns.auto.hide();
    btns.pause.hide();
    btns.resume.enable();
    btns.stop.enable();
    return false;
}

function resume() {
    // continue after pause
    if (!timer) timer = window.setTimeout(doNext, 50);
    btns.auto.hide();
    btns.pause.enable();
    btns.resume.hide();
    btns.stop.enable();
    return false;
}

function manual() {
    // stop automatic mode
    if (timer) clearTimeout(timer);
    timer = null;
    var wasPreloading = preloadServer;
    if (preloadServer) {
        // flush cache so that future pre-fetches will pre-fetch images
        cacheP = new Cache(50);
        cacheF = new Cache(50);
    }
    autoRoot = null;
    preloadServer = false;
    btns.auto.enable();
    btns.pause.hide();
    btns.resume.hide();
    btns.stop.disable();
    if (wasPreloading && thisPage) jumpTo(thisPage.path);
    return false;
}

function doParent(path) {
    // Jump to given path, cancelling auto-play
    if (autoRoot) manual();
    jumpTo(path);
    return false;
}

function up() {
    // Go up one folder level
    if (thisPage.lastParent != "") doParent(thisPage.lastParent);
    return false;
}

function hideShowActions() {
    // Hide or show the "actions" menu body
    var body = document.getElementById("actionsBody");
    var openClose = document.getElementById("openClose");
    if (body.style.display == 'none') {
        body.style.display = 'block';
        openClose.src = 'open.gif';
    } else {
        body.style.display = 'none';
        openClose.src = 'closed.gif';
    }
    return false;
}

function closeDlogs() {
    // Close all dialog elements
    for (var dlog in dlogs) {
        var style = dlogs[dlog].style;
        if (style.display != 'none') style.display = 'none';
    }
    return false;
}

function openDlog(dlogName) {
    closeDlogs();
    dlog = dlogs[dlogName];
    dlog.style.display = 'block';
    if (!dlogPositioned[dlogName]) {
        moveToCenter(dlog, wSize);
        dlogPositioned[dlogName] = true;
    }
    if (dlog == dlogs.editForm) {
        if (timer) pause();
        var editUser = document.getElementById("editUser");
        var editPwd = document.getElementById("editPwd");
        editUser.value = user;
        editPwd.value = pwd;
        if (user == "") {
            editUser.focus();
        } else if (pwd == "") {
            editPwd.focus();
        } else {
            editTitle.focus();
        }
    }
    return false;
}

var editStayOpen; // leave the dialog up after save completion

function editSave(stayOpen) {
    editStayOpen = stayOpen;
    user = document.getElementById("editUser").value;
    pwd = document.getElementById("editPwd").value;
    setCookie("pachyuser", user, null, "/");
    setCookie("photopwd", pwd, null, "/", null, true);
    var title = document.getElementById("editTitle").value;
    editInner.style.visibility = "hidden";
    writing.style.display = 'block';
    if (thisPage.isPhoto) {
        cacheP.flush(thisPage.path);
        cacheF.flush(thisPage.lastParent);
    } else {
        cacheP = new Cache(50);  // parent names might have changed
        cacheF = new Cache(50);
    }
    var now = new Date();
    var nowSec = Math.round(now.getTime() / 1000);
    var hmac = md5HmacHex(utf8("AndrewAlbumApp\x00xmlSave\x00" +
            user + "\x00" + nowSec + "\x00" + thisPage.path + "\x00" +
            title),
            utf8(pwd));
    var saveRequest = new Object();
    saveRequest.isPhoto = thisPage.isPhoto;
    get(saveRequest,
        "photos.php", "path=" + thisPage.path + "&op=xmlSave&title=" +
            encodeURIComponent(title) +
            "&user=" + encodeURIComponent(user) +
            "&time=" + nowSec +
            "&hmac=" + hmac);
    return false;
}

function starNumber(star) {
    var id = star.id;
    return parseInt(id.substring(4));
}

function setStarDisplay(n, src) {
    for (var i = 1; i <= n; i++) {
        var x = document.getElementById("star" + i);
        x.src = src;
    }
    for (var i = n+1; i <= 5; i++) {
        var x = document.getElementById("star" + i);
        x.src = "whiteStar.gif";
    }
}

function showStarLevel() {
    // display current star level, or hide the stars
    var stars = document.getElementById("stars");
    if (!thisPage || thisPage.starLevel == null) {
        stars.style.display = "none";
    } else {
        setStarDisplay(thisPage.starLevel, "redStar.gif");
        stars.style.display = "block";
    }
}

function starOver(star) {
    // mouse over a star
    setStarDisplay(starNumber(star), "blueStar.gif");
}

function starClick(star) {
    // mouse click on a star
    var n = starNumber(star);
    thisPage.starLevel = n;
    thisPage.responseXML.documentElement.setAttribute("starLevel", n);
    showStarLevel();
}

function init() {
    if (!document.getElementById) {
        alert('This browser has no "getElementById" support.' +
            ' Redirecting to the non-scripted page');
        location.replace("noscript.php");
        return;
    }
    dropZone = document.getElementById("dropZone");
    reading = document.getElementById("reading");
    scaling = document.getElementById("scaling");

    title = document.getElementById("titleText");
    title.innerHTML = "";
    parents = document.getElementById("parents");
    parentsTxt = parents.innerHTML;
    parents.innerHTML = "";
    showStarLevel();

    dlogs.about = document.getElementById("about");
    dlogs.details = document.getElementById("details");
    dlogs.editForm = document.getElementById("editForm");
    dlogs.link = document.getElementById("link");

    writing = document.getElementById("writing");
    editInner = document.getElementById("editInner");
    editTitle = document.getElementById("editTitle");
    linkAnchor = document.getElementById("linkAnchor");

    btns.auto = new Button("auto", "auto(event)",
        "Play all the photos in this folder, from here onward",
        "Play all the photos in this folder, including " +
        "those in its sub-folders");
    btns.pause = new Button("pause", "pause()",
        "Suspend the slideshow, remembering the starting position",
        "I'm not here");
    btns.pause.hide();
    btns.resume = new Button("resume", "resume()",
        "Resume the slideshow",
        "I'm not here");
    btns.resume.hide();
    btns.stop = new Button("stop", "manual()",
        "Stop the slideshow",
        "I'm not here");
    btns.prev = new Button("prev", "doPrev()",
        "Move to the previous photo",
        "Move to the previous folder");
    btns.next = new Button("next", "doNext()",
        "Move to the next photo",
        "Move to the next folder, including sub-folders");
    btns.skip = new Button("skip", "doSkip()",
        "Skip the remainder of this folder",
        "Move to the next folder, skipping this folder's sub-folders");
    btns.edit = new Button("edit", "openDlog('editForm')",
        "Edit this photo's title",
        "Edit this folder's title");

    iPhone = isIphone();
    if (iPhone) interval += 1000; // compensate for no image pre-fetch
    window.onresize = doResize;

    var dest = "";
    if (location.hash && location.hash != "") {
        dest = location.hash.substring(1);
    } else {
        var prevPath = getCookie("AndrewAlbumApp");
        if (prevPath) dest = prevPath;
    }
    user = getCookie("pachyuser");
    if (!user) user = "";
    dropZone.innerHTML = "";
    jumpTo(dest);

    // Enable the UI
    document.getElementById("parentBtns").style.visibility = "visible";
    document.getElementById("prevnextBtns").style.visibility = "visible";
    document.getElementById("privBtns").style.visibility = "visible";
}
End of listing