// generated on Tue Aug 21 12:12:04 2007
/*
 * (c) Copyright SXOOP Technologies Ltd. 2005-2007
 * All rights reserved.
 *
 * If you have purchased PXN8 for use on your own servers and want to change the 
 * core functionality we strongly recommend that 
 * You make a copy of this file and rename it to $YOURCOMPANY_pxn8core.js and use that
 * as a working copy.
 */

window.onerror = function(message,url,line)
{
    alert("A Javascript error occurred!\n" + message + "\non  line " + line);
    return false;
};

/**************************************************************************

SECTION: VARIABLES 
==================
Pixenate uses a number of javascript variables...

PXN8
====
PXN8 is the name of the global variable used by Pixenate to store variables and 
functions used by the Pixenate javascript API. PXN8 acts as a global namespace for all such
variables and functions.

***/
var PXN8 = PXN8 || {};

PXN8.server = document.location.protocol + "/" + "/" + document.location.host;

/**************************************************************************

PXN8.root
=========
Specifies where Pixenate&trade; is located relative to the Web
Root. If you install Pixenate in a directory other than one named
'pixenate' in the webroot folder, *You must change this value
accordingly*. For example if your webroot is /var/www/html and you have installed PXN8 
in /var/www/html/pixenate, then you should set PXN8.root = "/pixenate/".

Type
----
String

Default Value
-------------
    "/pixenate/"

***/
PXN8.root = "/pixenate/";

/**************************************************************************

PXN8.replaceOnSave
==================
replaceOnSave specifies how PXN8 handles image URLs. 
If set to true then PXN8 always assumes that the photo at the supplied URL has changed.
If set to false then PXN8 will assume that the photo at the supplied url hasn't changed since it was last retrieved.
If the photo URL maps to a filepath on the webserver and your photo-editing application 
overwrites the original file when saved then you should set this to true.
By default, it's set to true to avoid potential caching problems when save operation overwrites the original image.

Type
----
boolean

Default Value
-------------
    true


***/
PXN8.replaceOnSave = true;

/***************************************************************************

PXN8.aspectRatio
================
The currently enforced aspect-ratio which is enforced when the user
selects an area of the photo. If the value is anything other than...

    {width: 0, height: 0} 

...then that aspect ratio is enforced. E.g. to
enforce a 2x3 aspect ratio on selections...

    PXN8.selectByRatio("2x3");

Type
----
Object (with *width* and *height* properties). *READ ONLY*

Default Value
-------------
    {width: 0, height: 0}

***/
PXN8.aspectRatio =  {width:0 , height:0};

/**************************************************************************

PXN8.position
=============

The current mouse position on the photo. The PXN8.position property
takes into account the current magnification level so it is always the
position of the mouse relative to the top-left corner of the un-magnified photo.

Type
----
Object (with *x* and *y* properties). *READ ONLY*

Default Value
-------------
    {x: "-", y: "-"}

***/

PXN8.position = {x: "-", y: "-"}; 


/***************************************************************************

PXN8.style
==========
PXN8.style is a namespace used to define style-related variables used by Pixenate.

***/
PXN8.style = {};

/**************************************************************************

PXN8.style.notSelected
======================
This variable defines the opacity and color of the non-selected areas of the photo.

Type
----
Object (with *opacity* and *color* properties).

Default Value
-------------
    {opacity: 0.33, 
     color:   "black"}

***/
PXN8.style.notSelected = {opacity: 0.33,color: "black"};

/***************************************************************************

PXN8.style.resizeHandles
========================
Defines the color, and size (in pixels) of the resize handles which appear at 
the corners and sides of the selected area of the image.

Type
----
Object (with *color* and *size* properties).

Default Value
-------------
    {color: "white", 
     size:  6};
***/
PXN8.style.resizeHandles ={color: "white",size: 6,smallsize: 4,oldsize: -1};


/***************************************************************************

SECTION: CALLBACKS and Related Functions
========================================
Hooks can be added to Pixenate using the following pre-defined Pixenate event types.
For more information on adding hooks to Pixenate please refer to the *PXN8.listener* set
of functions.
***/

/**************************************************************************

PXN8.ON_IMAGE_LOAD
==================
This event fires whenever *a new photo* is loaded into the
web page as a result of an editing operation. It does not fire when
the user *Undoes* or *Redoes* an operation - to catch those events use *PXN8.ON_IMAGE_CHANGE*.

Examples
--------
    function myOnImageLoad(eventType){
        alert("A new image has been loaded");
    }
    PXN8.listener.add(PXN8.ON_IMAGE_LOAD,myOnImageLoad);

***/
PXN8.ON_IMAGE_LOAD = "ON_IMAGE_LOAD";

/***************************************************************************

PXN8.ON_IMAGE_CHANGE
====================
This event fires whenever the photo is changed as a result of an
editing operation (including the undo and redo family of operations). 

Examples
--------
    function myOnImageChange(eventType){
        alert("The image has been modified");
    }
    PXN8.listener.add(PXN8.ON_IMAGE_CHANGE,myOnImageChange);
***/
PXN8.ON_IMAGE_CHANGE =  "ON_IMAGE_CHANGE";

/****************************************************************************

PXN8.ON_ZOOM_CHANGE
===================
This event is fired whenever the magnification level of the photo has been changed
(when the user zooms in and out).

Examples
--------

    function myOnImageZoom(eventType){
        alert("You have zoomed the image to " + (PXN8.zoom.value() * 100) + "%");
    };
    PXN8.listener.add(PXN8.ON_IMAGE_ZOOM, myOnImageZoom);

***/
PXN8.ON_ZOOM_CHANGE =  "ON_ZOOM_CHANGE";

/*****************************************************************************

PXN8.ON_SELECTION_CHANGE
========================
This event fires whenever the selection area is modified. *Do
not do anything which requires User interaction (such as alert,
confirm, prompt etc) in your listener as this event can fire quite frequently* while 
the user is resizing, moving or initializing the selection area using the mouse. If you 
want your hook to be called after the user has finished selecting the area, then use
*PXN8.ON_SELECTION_COMPLETE* instead.

***/
PXN8.ON_SELECTION_CHANGE =  "ON_SELECTION_CHANGE";

/*****************************************************************************

PXN8.ON_SELECTION_COMPLETE
==========================
This event fires when the user mouseups after making a selection or when a selection
has been made programmatically.

Examples
--------

    function myOnSelectionComplete(eventType){
        var log = document.getElementById("my_log");
        var sel = PXN8.getSelection();
        alert("You selected: " + sel.top + "," + sel.left + "," + 
              sel.width + "," + sel.height);
    }
    PXN8.listener.add(PXN8.ON_SELECTION_COMPLETE,myOnSelectionComplete);

***/
PXN8.ON_SELECTION_COMPLETE =  "ON_SELECTION_COMPLETE";

/*****************************************************************************

PXN8.ON_IMAGE_ERROR
===================
This event is fired when an image update fails or an image fails to load.

***/
PXN8.ON_IMAGE_ERROR =  "ON_IMAGE_ERROR";

/* ============================================================================
 *
 * Functions related to PXN8 listeners
 */
PXN8.listener = {
    /**
     * A map of listeners by event type
     */
    listenersByType : {}
};
PXN8.listener.listenersByType[PXN8.ON_ZOOM_CHANGE] = [];
PXN8.listener.listenersByType[PXN8.ON_SELECTION_CHANGE] = [];
PXN8.listener.listenersByType[PXN8.ON_IMAGE_CHANGE] = [];
PXN8.listener.listenersByType[PXN8.ON_IMAGE_ERROR] = [];

/****************************************************************************

PXN8.listener.add()
===================
Adds a new callback function to the list of functions to be called when a PXN8 event occurs.
Listeners can be added for the following event types...
* PXN8.ON_ZOOM_CHANGE : Fired when the image is zoomed in or out.
* PXN8.ON_SELECTION_CHANGE : Fired when the selection has changed (and during a manual selection operation).
* PXN8.ON_SELECTION_COMPLETE : Fired when the user has completed making a selection or when the selection has changed programattically.
* PXN8.ON_IMAGE_CHANGE : Fired when the image has been modified. This is fired *after* the changed image has been loaded into the browser.

Parameters
----------
* eventType : See above event types.
* callback : A function which will be called when an event of eventType fires. This should be a 
javascript function reference or literal. The callback should take a single parameter called eventType which will be 
one of the above defined event types.

Returns
-------
The supplied callback parameter.

Example
-------
The following snippet of code displays an alert message every time the image is modified.

    // add an anonymous function as a listener
    //
    var myListener = PXN8.listener.add(PXN8.ON_IMAGE_CHANGE, function(eventType){
        if (eventType == PXN8.ON_IMAGE_CHANGE){
           alert("The image has changed");
        }
    });

The code above is equivalent to...

    // define and name the function
    function myListener(eventType){
        if (eventType == PXN8.ON_IMAGE_CHANGE){
           alert("The image has changed");
        }
    }

    // add the named function
    PXN8.listener.add(PXN8.ON_IMAGE_CHANGE,myListener);

Related
-------
PXN8.listener.remove PXN8.listener.onceOnly

***/
PXN8.listener.add = function (eventType,callback) 
{
    var self = PXN8.listener;
    
    var callbacks = self.listenersByType[eventType];
    var found = false;
    if (!callbacks){
        callbacks = [];
        self.listenersByType[eventType] = callbacks;
    }
    for (var i = 0;i < callbacks.length; i++){
        if (callbacks[i] == callback){
            found = true;
            break;
        }
    }
    if (!found){
        callbacks.push (callback);
    }
    return callback;
    
};
/***************************************************************************

PXN8.listener.remove()
======================
Removes a callback function from the list of functions to be called when a PXN8 event occurs.


Parameters
----------

* eventType : The type of event for which you want to remove the listener (A listener
can potentially listen for different types of events).
* callback : a function reference which will be removed from Pixenate's list of listeners
for that particular event type.

Example
-------

    PXN8.listener.remove(PXN8.ON_IMAGE_CHANGE,myListener);

Related
-------
PXN8.listener.add PXN8.listener.onceOnly

***/
PXN8.listener.remove = function (eventType, callback)
{
    var self = PXN8.listener;
    
    var callbacks = self.listenersByType[eventType];
    if (!callbacks) return;
    for (var i = 0;i < callbacks.length; i++){
        if (callbacks[i] == callback){
            callbacks.splice(i,1);
            i--;
        }
    }
};
/****************************************************************************

PXN8.listener.onceOnly()
========================
Add a special-case of listener that *will only be invoked once and once only*.

Parameters
----------
* eventType : The type of event for which you want to listen (once only).
* callback : The function to be called when the event occurs (only called once then removed from list).

Returns
-------
The newly added Listener.

Related
-------
PXN8.listener.add PXN8.listener.remove

***/
PXN8.listener.onceOnly = function (eventType,callback)
{
    var self = PXN8.listener;
    callback.onceOnly = true;
    return self.add(eventType, callback);
};


/*
 * What is the current operation number ?
 */
PXN8.opNumber =  0;

/**
 * what is the total number of operations performed ?
 */
PXN8.maxOpNumber =  0;


/**
 * The JSON response from the last image operation
 */
PXN8.response =  {
    status: "", 
    image: "", 
    errorCode: 0, 
    errorMessage: "" 
};

/**
 * If an operation is performed on an image then this is set to true
 * until the image update has completed
 */
PXN8.updating =  false;

/**
 * The upper bounds on image sizes
 */
PXN8.resizelimit = { 
    width: 1600,
    height: 1200
};





/*
 * A hashtable of images with the image.src url as the key (value is 'true')
 * Need this for IE to force onload handler for images which 
 * have already been loaded.
 */
PXN8.imagesBySrc =  {};

// the start of the selection along the X axis (from left)
PXN8.sx =  0;

// the start of the selection along the Y axis (from top)
PXN8.sy =  0;

// the end of the selection along the X axis
PXN8.ex =  0;

// the end of the selection along the Y axis 
PXN8.ey =  0;

/***************************************************************************

SECTION: Core Pixenate Functions
================================
Pixenate's core javascript API relies on the following functions...

***/

/***************************************************************************

PXN8.initialize()
=================
Call this function to initialize the PXN8 editor.

Parameters
----------
* image_url : A string value which is an Image URL of any of the forms specified below.

The image_url parameter can be any of the following. 

* full URL : E.g. "http://pixenate.com/pixenate/images/samples/hongkong.jpg" 
* absolute path (relative to domain) : E.g. "/pixenate/images/samples/hongkong.jpg"
* relative path (relative to page) : E.g. "../../images/samples/hongkong.jpg" 


Alternatively it can be an object with 2 attributes *url* (see above) and *filepath*. 
The filepath should be a path relative to where Pixenate is installed and 
which can be used by the Pixenate server CGI to access the image from the server's filesystem.  

Example
-------
Pixenate can be initialized anywhere on the page *as long as the PXN8.initialize() function is called after 
the 'pxn8_canvas' div has been parsed by the browser*. For example the following code won't work on many browsers...

*WRONG*

    <script type="text/javascript">PXN8.initialize("http://pixenate.com/pixenate/images/samples/hongkong.jpg");</script>
    <div id="pxn8_canvas"></div>

The correct approach is as follows...

    <div id="pxn8_canvas"></div>
    <!-- declare the pxn8_canvas BEFORE calling initialize -->
    <script type="text/javascript">PXN8.initialize("http://pixenate.com/pixenate/images/samples/hongkong.jpg");</script>
    
Another approach is to call PXN8.initialize() when the *window.onload* event fires...

    <!-- the following javascript block can appear anywhere on the page -->
    <script type="text/javascript">
      PXN8.dom.addLoadEvent(function(){
          // pxn8_canvas will have been parsed by the time this javascript is executed.
          PXN8.initialize("http://pixenate.com/pixenate/images/samples/hongkong.jpg");
      });
    </script>

Related
-------
PXN8.dom.addLoadEvent

***/
PXN8.initialize = function( param ) 
{
    PXN8.ready = false;
    
    var _ = PXN8.dom;

    var image_src;
    
    var paramType = typeof param;
    
    if (paramType == 'string'){
        image_src = param;
    }else{
        image_src = param.url;
    }
    
    PXN8.priv.createSelectionRect();

    var canvas = PXN8.initializeCanvas();
    
    //
    // create the pxn8_image_container element if not already present
    //
    var imgContainer = _.id("pxn8_image_container");
    if (!imgContainer){
        imgContainer = _.ac(canvas,_.ce("div",{id: "pxn8_image_container"}));
        //
        // FIX for IE's broken handling of Faded JPEGS (introduction of white-noise)...
        // IE interprets a completely black pixel in a JPEG as being transparent.
        // Because of this, in some dark areas there will be white pixels. This is
        // the background color showing through.
        // the solution is to change the background color of the pxn8_image_container
        // to black or change the #00000 pixels to #000001 
        // see http://www.alexjudd.com/?p=5
        //
        imgContainer.style.backgroundColor = "white";
        
    }
    
    //
    // create and style the divs that will bound the selection area
    //
    var rects = ["pxn8_top_rect","pxn8_bottom_rect","pxn8_left_rect","pxn8_right_rect"];
    for (var i = 0;i < rects.length; i++){
        var rect = _.id(rects[i]);
        if (!rect){
            rect = _.ac(canvas,_.ce("div",{id: rects[i]}));
        }

        rect.style.fontSize = "0px";
        if (!rect.style.backgroundColor){
            rect.style.backgroundColor = PXN8.style.notSelected.color;
        }
        rect.style.position = "absolute";
        if (!rect.style.opacity){
            _.opacity(rect,PXN8.style.notSelected.opacity);
        }
        
        rect.style.top = "0px";
        rect.style.left = "0px";
        rect.style.width = "0px";
        rect.style.height = "0px";
        rect.style.display = "none";
        rect.style.zIndex = 1;

    }

    PXN8.image.location = image_src;

    PXN8.opNumber = 0;
    PXN8.maxOpNumber = 0;
    
    PXN8.history = new Array();
    //
    // initialize offsets (for undo and redo of composite operations)
    //
    PXN8.offsets = [1];
    
    //
    // wph 20070123 : If the image URL passed to PXN8.initialize() is of the form...
    //
    // ../path/to/images/x.jpg 
    // ../../gallery/x.jpg
    // 
    // then 

    var fetchOp = {
        operation: "fetch", 
        url: escape(PXN8.image.location)
    };
    fetchOp.pxn8root = PXN8.root;
    if (paramType == 'object'){
        for (var i in param){
            fetchOp[i] = param[i];
        }
    }
    
    PXN8.history.push(fetchOp);
    

    if (PXN8.replaceOnSave){
        fetchOp.random = PXN8.randomHex();
    }
    
    var pxn8image = _.id("pxn8_image");

    /**
     * Safari doesn't load the image immediately
     * so setting the PXN8.image.width & height variables
     * makes no sense until the image has loaded.
     * the following function gets called directly from within
     * this function but also from within the img.onload function
     * if no <img id="pxn8_image".../> element appears in the body
     * (if pxn8_image is created dynamically as is the case with a 
     * toolbar theme.
     *
     */
    var onImageLoad = function(){
        
        var _ = PXN8.dom;
        
        var pxn8image = _.id("pxn8_image");
        
        PXN8.image.width =  pxn8image.width;
        PXN8.image.height = pxn8image.height;

        PXN8.priv.addImageToHistory(pxn8image.src);
        PXN8.show.size();
    };
    
    
    /**
     *  Initialize the image
     */
    if (!pxn8image){
        //
        // this won't work for Safari.
        // it is recommended that the <img> tag always appears
        // inside the pxn8_image_container tag.
        //

        // wph 20070117 : Use of innerHTML instead of DOM forces IE to load the image at the correct dimensions
        // Upload -> Crop -> save as same name -> upload same file : Image appears stretched to old dimensions
        //pxn8image = dom.ac(imgContainer,dom.ce("img",{id: "pxn8_image", src: PXN8.image.location}));

        var innerHTML = '<img id="pxn8_image" border="0" src="' + PXN8.image.location + '"/>';

        try {
            imgContainer.innerHTML = innerHTML;
        }catch (e){
            alert("An error occurred while adding the <img> tag to the page.\n" +
                  "This is most likely because the pxn8_canvas DIV has been added to an incorrect element (<table> or <p>).\n"  + 
                  "The error message reported was: " + e.message);
            PXN8.dom.ac(imgContainer,PXN8.dom.ce("img",{id: "pxn8_image", src: PXN8.image.location}));
        }

        pxn8image = _.id("pxn8_image");
        
        //PXN8.listener.add(PXN8.ON_IMAGE_LOAD,onImageLoad);

        pxn8image.onload = onImageLoad;


    }else{

        // 
        //  The image is already present - re-add it to the DOM to ensure the 
        //  correct dimensions are applied.
        // 
        var imgContainer = _.cl("pxn8_image_container");
        //
        // wph 20060905 : Must change the image src attribute whenever PXN8.initialize is called
        // e.g. if there is a web-page with thumbnail images which change the current image for 
        // editing, the .src attribute *MUST* be updated !
        

        // wph 20070117 : Use of innerHTML instead of DOM forces IE to load the image at the correct dimensions
        // Upload -> Crop -> save as same name -> upload same file : Image appears stretched to old dimensions
        //pxn8image = dom.ac(imgContainer,dom.ce("img",{id: "pxn8_image", src: PXN8.image.location}));

        var innerHTML = '<img id="pxn8_image" border="0" src="' + PXN8.image.location + '"/>';
        try {
            imgContainer.innerHTML = innerHTML;
        }catch(e){
            alert("An error occurred while adding the <img> tag to the page.\n" +
                  "This is most likely because the pxn8_canvas DIV has been added to an incorrect element (<table> or <p>).\n"  + 
                  "The error message reported was: " + e.message);
            PXN8.dom.ac(imgContainer,PXN8.dom.ce("img",{id: "pxn8_image", src: PXN8.image.location}));
        }
        
        pxn8image = _.id("pxn8_image");

        pxn8image.onload = onImageLoad;
        
        //PXN8.listener.add(PXN8.ON_IMAGE_LOAD,onImageLoad);

    }

    PXN8.listener.onceOnly(PXN8.ON_IMAGE_LOAD,function(){

        // wph 20070124 
        // update the fetch.url attribute to reflect the canonical image location
        // e.g. ../../images/samples/greenleaves.jpg becomes...
        // http://mydomain/pixenate/images/samples/greenleaves.jpg
        // This is important when the image passed in is a relative path to the current page.

        var fetchOp = PXN8.getOperation(0);
        var theImage = document.getElementById("pxn8_image");

        //
        // wph 20070227 : must escape the URL to avoid the case where an image url contains an '&' character
        // in which case the 'script' parameter passed to pxn8.pl was being truncated at first '&' character
        //
        fetchOp.url = escape(theImage.src);
        //
        // wph 20070201 
        // Set the global PXN8.ready flag to true so that operations can be performed
        // on the image.
        //
        PXN8.ready = true;
        
    });

    //
    // wph 20060714 notify ON_IMAGE_LOAD listeners
    //
    PXN8.event.removeListener(pxn8image,"load",PXN8.imageLoadNotifier);
    PXN8.event.addListener(pxn8image,"load",PXN8.imageLoadNotifier);

    /* initialize zoom info */
    PXN8.show.zoom();

};


/***************************************************************************

PXN8.select()
=============
Selects an area of the image. Use this function to programmatically select
an area of the image.

Parameters
----------
* left : The start position of the selected area along the X axis (starts at left).
* top : The start position of the selected area along the Y axis (starts at top).
* width : The width of the selected area.
* height : The height of the selected area.

Alternatively you can provide a single object as a parameter (with the following properties)...

* left : The start position of the selected area along the X axis (starts at left).
* top : The start position of the selected area along the Y axis (starts at top).
* width : The width of the selected area.
* height : The height of the selected area.

Example
-------
The following example code will select the top half of the photo...

    var theImage = document.getElementById("pxn8_image");
    var zoomValue = PXN8.zoom.value();
    //
    // the image's width and height might not reflect the true width and height if the image
    // has been zoomed in or out.
    //
    var realWidth = theImage.width / zoomValue;
    var realHeight = theImage.height / zoomValue;

    PXN8.select(0, 0, realWidth, realHeight / 2);

Related
-------
PXN8.getSelection PXN8.selectByRatio

***/
PXN8.select = function (startX, startY, width, height)
{
    var self = PXN8;
    
    if (typeof startX == "object"){
        var sel = startX;
        startY = sel.top;
        width = sel.width;
        height = sel.height;
        startX = sel.left;
    }
    
    self.sx = startX;
    self.sy = startY;
    self.ex = self.sx + width;
    self.ey = self.sy + height;
    
    if (self.sx < 0) self.sx = 0;
    if (self.sy < 0) self.sy = 0;
    if (self.ex > PXN8.image.width) self.ex = PXN8.image.width;
    if (self.ey > PXN8.image.height) self.ey = PXN8.image.height;
    
    self.selectArea();
};

/****************************************************************************

PXN8.getSelection()
===================
Return a Rect that represents the current selection.

Returns
-------
An object with the following properties...
* top : The topmost coordinate on the Y axis of the selected area.
* left: The leftmost coordinate on the X axis of the selected area.
* width: The width of the selected area.
* height: The height of the selected area.

Example
-------
    var selectedArea = PXN8.getSelection();
    alert ("You have selected an area " + selectedArea.width + "x" + selectedArea.height );
Related
-------
PXN8.select PXN8.selectByRatio

***/
PXN8.getSelection = function()
{
    var rect = {};
    var self = PXN8;
    
    rect.width = self.ex>self.sx?self.ex-self.sx:self.sx-self.ex;
    rect.height = self.ey>self.sy?self.ey-self.sy:self.sy-self.ey;
    rect.left = self.ex>self.sx?self.sx:self.ex;
    rect.top = self.ey>self.sy?self.sy:self.ey;
    rect.left = rect.left<0?0:rect.left;
    rect.top = rect.top<0?0:rect.top;     
    return rect;
};

/****************************************************************************

PXN8.selectByRatio()
====================
Selects an area using an aspect ratio of the form "WxH" where W is width and H is height.

Parameters
----------
* ratio : The ratio is expressed as a string e.g. "4x6".
* override (optional) : A boolean value indicating whether or not to ignore the images's dimensions (don't optimize selection size). 
The default value if none is specified is *false*.

Example
-------

Assuming the user is working with a photo which is 300x225, calling 
PXN8.selectByRatio("2x3") will result in the following selection...
    {width: 300, height: 200, left: 0, top: 12}
<table>
  <tr><td>Original</td><td>PXN8.selectByRatio("2x3")</td></tr>
  <tr>
     <td valign="top"><img src="pigeon300x225.jpg"/></td>
     <td valign="top"><img src="pigeon300x225sbr2x3.jpg"/></td>
  </tr>
  <tr><td>Original</td><td>PXN8.selectByRatio("2x3",true)</td></tr>
  <tr>
     <td valign="top"><img src="pigeon300x225.jpg"/></td>
     <td valign="top"><img src="pigeon300x225sbr2x3true.jpg"/></td>
  </tr>
</table>

Related
-------
PXN8.select PXN8.selectAll PXN8.unselect PXN8.getSelection

***/
PXN8.selectByRatio = function(ratio,override)
{
    var _ = PXN8.dom;
    var self = PXN8;
    
    if (typeof ratio != "string"){
        alert("Ratio must be expressed as a string e.g. '4x6'");
        return;
    }
    
    var pair = /^([0-9]+)x([0-9]+)/;
    var match = ratio.match(pair);
    if (match != null){
        var rw = parseInt(match[1]);
        var rh = parseInt(match[2]);
        
        if (override){
            PXN8.aspectRatio.width = rw;
            PXN8.aspectRatio.height = rh;
        }else{
            if (PXN8.image.width > PXN8.image.height){
                if (rw > rh){
                    PXN8.aspectRatio.width = rw;
                    PXN8.aspectRatio.height = rh;
                }else{
                    PXN8.aspectRatio.width = rh;
                    PXN8.aspectRatio.height = rw;
                }
            }else{
                if (rw > rh){
                    PXN8.aspectRatio.width = rh;
                    PXN8.aspectRatio.height = rw;
                }else{
                    PXN8.aspectRatio.width = rw;
                    PXN8.aspectRatio.height = rh;
                }
            }
        }
    }else{
        PXN8.aspectRatio.width = 0;
        PXN8.aspectRatio.height = 0;

        PXN8.resize.enable(["n","s","e","w"],true);
        
        return;
    }
        
    self.sx = 0;
    self.sy = 0;
    
    var t1 = PXN8.image.width / PXN8.aspectRatio.width ;
    var t2 = PXN8.image.height / PXN8.aspectRatio.height ;
    if (t2 < t1){
        self.ey = PXN8.image.height;
        self.ex = Math.round(self.ey / PXN8.aspectRatio.height * PXN8.aspectRatio.width);
    }else{
        self.ex = PXN8.image.width;
        self.ey = Math.round(self.ex / PXN8.aspectRatio.width * PXN8.aspectRatio.height);
    }
    self.sx = Math.round((PXN8.image.width - self.ex) / 2);
    self.sy = Math.round((PXN8.image.height - self.ey) / 2);
    self.ex += self.sx;
    self.ey += self.sy;

    PXN8.resize.enable(["n","s","e","w"],false);

    self.selectArea();
};



/****************************************************************************

PXN8.rotateSelection()
======================
Rotates the selection area by 90 degrees.

Example
-------
<table>
  <tr><td>Before</td><td>After PXN8.rotateSelection()</td></tr>
  <tr>
     <td valign="top"><img src="pigeon300x225b4rotatesel.jpg"/></td>
     <td valign="top"><img src="pigeon300x225afterrotatesel.jpg"/></td>
  </tr>
</table>

Related
-------
PXN8.select PXN8.selectByRatio PXN8.selectAll PXN8.unselect PXN8.getSelection

***/
PXN8.rotateSelection = function()
{
    var sel = PXN8.getSelection();
    var cx = sel.left + (sel.width / 2);
    var cy = sel.top + (sel.height / 2);

    
    PXN8.select (cx - sel.height/2, cy - sel.width /2, sel.height, sel.width);

    //
    // swap width and height of aspectRatio
    //
    var temp = PXN8.aspectRatio.width;
    
    PXN8.aspectRatio.width = PXN8.aspectRatio.height;
    PXN8.aspectRatio.height = temp;
    //
    // snap to the current enforced aspect ratio
    //
    PXN8.snapToAspectRatio();
    
};

/****************************************************************************

PXN8.selectAll()
================
Selects the entire photo area.

Example
-------
<table>
  <tr><td>Before</td><td>After *PXN8.selectAll()*</td></tr>
  <tr>
     <td valign="top"><img src="pigeon300x225.jpg"/></td>
     <td valign="top"><img src="pigeon300x225selectall.jpg"/></td>
  </tr>
</table>

Related
-------
PXN8.select PXN8.selectByRatio PXN8.rotateSelection PXN8.unselect PXN8.getSelection
***/
PXN8.selectAll = function() 
{
    PXN8.sx = 0;
    PXN8.sy = 0;
    PXN8.ex = PXN8.image.width;
    PXN8.ey = PXN8.image.height;
    PXN8.selectArea(); 
};
/****************************************************************************

PXN8.unselect()
===============
Unselect the entire photo. The selection will be discarded.

***/
PXN8.unselect = function () 
{
    var _ = PXN8.dom;
    var self = PXN8;
    
    self.sx = 0;
    self.sy = 0;
    self.ex = 0;
    self.ey = 0;   
    var selectionDiv = _.id("pxn8_select_rect");
    selectionDiv.style.display = "none";
    
    
    var topRect = _.id("pxn8_top_rect");
    topRect.style.borderWidth = "1px";
    
    topRect.style.display = "none";
    
    var bottomRect = _.id("pxn8_bottom_rect");
    bottomRect.style.borderWidth = "1px";
    
    bottomRect.style.display = "none";
    
    var leftRect = _.id("pxn8_left_rect");
    leftRect.style.display = "none";
    leftRect.style.borderWidth = "0px";
    
    
    _.id("pxn8_right_rect").style.display = "none";
    
    /*
     * update the field values
     */  
    PXN8.show.position();

    PXN8.show.selection();
    
    self.listener.notify(PXN8.ON_SELECTION_CHANGE);
};





/* ============================================================================
 *
 * Miscellaneous top-level functions
 *
 */

/***************************************************************************

PXN8.getUncompressedImage()
===========================
Returns the relative URL to the uncompressed 100% full quality image.
This version of the image is not normally downloaded and displayed in the browser
during an editing session because it is typically much larger than the more bandwidth-friendly
lower resolution image (typically using 85% quality).

Returns
-------
A path (relative the PXN8.root) to the uncompressed image.

Examples
--------

    var uncompressed = PXN8.getUncompressedImage();
    // uncompressed = "cache/01_feabcdd1d0workingjpg.jpg";
    // view the image
    document.location = PXN8.server + PXN8.root + "/" + uncompressed;

***/
PXN8.getUncompressedImage = function()
{
    if (PXN8.responses[PXN8.opNumber]){
        return PXN8.responses[PXN8.opNumber].uncompressed;
    } else {
        return false;
    }
};


/**
 * -- function:    PXN8.listener.notify
 * -- description: Called by various methods to notify listeners  
 * -- param eventType (ON_ZOOM_CHANGE, ON_IMAGE_CHANGE, ON_SELECTION_CHANGE etc)
 */ 
PXN8.listener.notify = function(eventType,source)
{
    var self = PXN8.listener;
    var listeners = self.listenersByType[eventType];
    if (listeners){
        for (var i = 0; i < listeners.length; i++){
            var listener = listeners[i];
            if (listener != null){
                listener(eventType,source);
                if (listener.onceOnly){
                    PXN8.listener.remove(eventType,listener);
                    i--;
                }
                
            }
        }
    }
};

/**
 * This function should be the first ON_IMAGE_LOAD function called.
 * It does all of the housekeeping necessary for PXN8. 
 * All other ON_IMAGE_LOAD callbacks should be called after this !!!
 */
PXN8.imageLoadHousekeeping = function(eventType,theImage){

    var _ = PXN8.dom;

    theImage = _.id("pxn8_buffered_image");

    if (theImage == null){
        theImage = _.id("pxn8_image");
    }
    
    PXN8.log.append("image " + theImage.src + " has loaded");	     

    PXN8.image.width = theImage.width;
    PXN8.image.height = theImage.height;

    /**
     * wph 20070630 : now we have the original size of the buffered image, 
     * change it's width & height attributes to match the zoomed size before 
     * copying the buffer to the display.
     */
    
    var zoomFactor = PXN8.zoom.value();
    var ow = theImage.width;
    var oh = theImage.height;
    var nw = ow * zoomFactor;
    var nh = oh * zoomFactor;
    
    theImage.width = nw;
    theImage.height = nh;

    //
    // now display the buffer contents
    //
    _.ac(_.cl("pxn8_image_container"),theImage);    
    theImage.id = "pxn8_image";

    PXN8.show.size();
    
    PXN8.priv.addImageToHistory(theImage.src);
    
    if (PXN8.sx > PXN8.image.width || 
        PXN8.ex > PXN8.image.width || 
        PXN8.sy > PXN8.image.height || 
        PXN8.ey > PXN8.image.height)
    {
        PXN8.unselect();
    }else{
        // the surrounding darkened rects might now 
        // extend beyond the bounds of the image 
            // so ...
        PXN8.selectArea();
    }
    
    PXN8.imagesBySrc[theImage.src] = true;
    PXN8.listener.notify(PXN8.ON_IMAGE_CHANGE);
    PXN8.show.zoom();


    var timer = _.id("pxn8_timer");
    if (timer){
        timer.style.display = "none";
    }

    return theImage;
    
};
/**
 * N.B. This should be the first call to PXN8.listener.add(PXN8.ON_IMAGE_LOAD,...)
 * Housekeeping should be done _BEFORE_ all other ON_IMAGE_LOAD handlers are called
 */
PXN8.listener.add(PXN8.ON_IMAGE_LOAD,PXN8.imageLoadHousekeeping);

/* ============================================================================ 
 *
 * the log object has a single method 'append'
 * If your edit page has an element with id 'pxn8_log' then ...
 * PXN8.log.append('hello world') 
 * ... will append a new paragraph with 'hello world' as the text to the div.
 */
PXN8.log = { };

PXN8.log.append = function(str)
{
    var _ = PXN8.dom;
    
    var log = _.id("pxn8_log");
    if (log){
        if (typeof str == "string"){
            var line = _.ce("div");
            _.ac(line,_.tx(str));
            _.ac(log,line);
        }else if (typeof str == "object"){
            var s = PXN8.objectToString(str);
            var line = _.ce("div");
            line.appendChild(_.tx(s));
            _.ac(log,line);
        }
    }
};

/* ============================================================================ */



/**
 * wph 20070124
 * Return an operation based on the operation number.
 * Unlike PXN8.getScript() this returns a reference to the operation object - not a copy.
 * Changes to the returned object will be reflected the next time a server-call is made.
 */
PXN8.getOperation = function(i)
{
    if (i > PXN8.opNumber){
        return null;
    }
    return PXN8.history[i];
};
/**
 * Return an image operation where index is the user-op number 
 * 
 */
PXN8.getUserOperation = function(index)
{
    var self = PXN8;
    var result = null;
    var lastIndex = 0;
    for (var i = 0;i < index; i++){
        lastIndex += self.offsets[i];
    }
    return self.history[lastIndex];
};

    
/***************************************************************************

PXN8.getScript()
================
Return a list (a copy) of all the operations which have been performed (doesn't include undone operations).

Returns
-------
A array of objects each of which is a distinct operation which was performed on the image.

Examples
--------
The following code retrieves all of the operations performed in the current editing session and displays a series 
of alerts telling the user what they have done.

    var whatYouDid = PXN8.getScript();
    for (var i = 0; i < whatYouDid.length; i++){
        alert("you performed a '" + whatYouDid[i].operation + "' operation");
    }

***/
PXN8.getScript = function()
{
    var self = PXN8;
    
    var result = new Array();

    //
    // WPH first get the real index of the last operation (this will not
    // necessarily be opNumber . E.g. if the user performs the following operations...
    // 
    // [1] Rotate
    // [2] Enhance + Normalize (2 operations combined into one single user operation)
    // [3] Redeye
    // ... then the history, offsets and opNumber values will be as follows
    //
    // history  [fetch,rotate,enhance,normalize,redeye]
    // offsets  [1,1,2,1]
    // opNumber 3

    var lastIndex = 0;
    for (var i = 0;i <= self.opNumber; i++){
        lastIndex += self.offsets[i];
    }

    //for (var i = 0;i <= self.opNumber; i++){
    for (var i = 0;i < lastIndex; i++){

        var original = self.history[i];
        //
        // make a copy of the object
        //
        var duplicate = {};
        for (var j in original){
            duplicate[j] = original[j];
        }
        result.push(duplicate);
    }

    return result;
};
/***************************************************************************

PXN8.isUpdating()
=================
Is pixenate currently updating the photo ?

Returns
-------
*true* or *false* depending on whether the photo is currently being updated.

***/
PXN8.isUpdating = function()
{
    return PXN8.updating;
};

/**
 * -- function curry
 * -- description Currying is a way of 'baking-in' an object to a function
 * Its a way of permanently binding an object and a function together
 * in effect create a new distinct function with the object embedded in it.
 * It's one of the cool higher-order programming features of dynamic languages
 * like Javascript and Perl.
 * PXN8.curry is a functor - a function which returns a function
 * -- param object The object to be baked in to the function
 * -- param func The function into which the object will be baked. 
 */
PXN8.curry = function(func,object)
{
    return function(){
        return func(object);
    };
};

/*
 * Update the display so that it reflects the current selection rectangle
 */
PXN8.selectArea = function()
{
    var _ = PXN8.dom;
    var self = PXN8;
    
    var selectRect = _.id("pxn8_select_rect");
    var theImg = _.id("pxn8_image");
    var leftRect = _.id("pxn8_left_rect");
    var rightRect = _.id("pxn8_right_rect");
    var topRect = _.id("pxn8_top_rect");
    var bottomRect = _.id("pxn8_bottom_rect");
    /*
     * has any selection been made yet ?
     */
    if (self.sx <=0 && self.sy <= 0 && self.ex <= 0 && self.ey <= 0){
        selectRect.style.display = "none";
        leftRect.style.display = "none";
        rightRect.style.display = "none";
        topRect.style.display = "none";
        bottomRect.style.display = "none";

        PXN8.listener.notify(PXN8.ON_SELECTION_CHANGE);
        
        return;
    }


    var sel = PXN8.getSelection();
    
    /*
     * update the field values
     */  
    PXN8.position.x = sel.left;
    PXN8.position.y = sel.top;
    PXN8.show.position();

    var zoom = PXN8.zoom.value();
    
    for (var i in sel){
        // watch out for prototype !
        if (typeof sel[i] != "function"){
            sel[i] = Math.floor(sel[i] * zoom);
        }
    }
    
    if (sel.left + sel.width > theImg.width || sel.top + sel.height > theImg.height){
        return;
    }
    
    leftRect.style.display = "block";
    leftRect.style.top = "0px";
    leftRect.style.left = "0px";
    leftRect.style.width =  sel.left + "px";
    leftRect.style.height = theImg.height + "px";
    
    rightRect.style.display = "block";
    rightRect.style.top = "0px";
    rightRect.style.left = sel.left + sel.width + "px";
    rightRect.style.width = (theImg.width - (sel.left + sel.width)) + "px";
    rightRect.style.height = theImg.height + "px";
    
    topRect.style.display = "block";
    topRect.style.top = "0px";
    topRect.style.left = sel.left + "px";
    topRect.style.width = sel.width + "px";
    topRect.style.height = sel.top + "px";

    bottomRect.style.display = "block";
    bottomRect.style.top = sel.top + sel.height + "px";
    bottomRect.style.left = sel.left + "px";
    bottomRect.style.width = sel.width + "px";
    bottomRect.style.height = theImg.height - (sel.top + sel.height) + "px";
    
    selectRect.style.top  = sel.top + "px";
    selectRect.style.left = sel.left + "px";
    selectRect.style.width = sel.width + "px";
    selectRect.style.height = sel.height + "px";
    selectRect.style.display = "block";
    
    selectRect.style.zIndex = 100;
    PXN8.show.selection();
    
    PXN8.listener.notify(PXN8.ON_SELECTION_CHANGE);
};

/*
 * Update the UI to inform the user that the image is being updated 
 * The msg param is optional - it contains text that will be displayed in
 * the *pxn8_timer* DIV. In most cases this is simply 'Updating image. Please wait...'
 * but it can be different - e.g. 'Saving image. Please wait...'
 */
PXN8.prepareForSubmit = function(msg)
{
    var _ = PXN8.dom;

    if (!msg){
        msg = PXN8.strings["UPDATING"];
    }        

    var timer = _.id("pxn8_timer");
    if (!timer){
        timer = _.ce("div",{id: "pxn8_timer"});
        _.ac(timer,_.tx(msg));
	     var canvas = _.id("pxn8_canvas");
        _.ac(canvas,timer);
    }
    if (timer){
        _.ac(_.cl(timer),_.tx(msg));
        timer.style.display = 'block';
        var theImage = _.id("pxn8_image");
        var imagePos = _.ep(theImage);
        timer.style.width  = Math.max(200,theImage.width) + "px";
    }
    PXN8.updating = true;
};

/**
 * For a given point calculate it's real location when 
 * the scroll area is taken into account.
 */
PXN8.scrolledPoint = function (x,y)
{
    var result = {"x":x,"y":y};

    var canvas = document.getElementById("pxn8_canvas");
    if (canvas.parentNode.id == "pxn8_scroller"){
        var scroller = document.getElementById("pxn8_scroller");
        result.x += scroller.scrollLeft;
        result.y += scroller.scrollTop;
    }
    return result;
};
    

/**
 * -- function PXN8.createPin
 * -- description Create a pin for placing on top of an image
 * -- param pinId The unique Id to be given to the created pin image
 * -- param imgSrc The image src attribute
 */
PXN8.createPin = function (pinId,imgSrc)
{
    var pinElement = document.createElement("img");
    pinElement.id = pinId;
    pinElement.className = "pin";
    pinElement.src = imgSrc;
    pinElement.style.position = "absolute";
    return pinElement;
};

/**
 * -- function mousePointToElementPoint
 * -- description Convert a mouse event point to a relative point for a given element
 * -- param mx The x value for the mouse event
 * -- param my The y value for the mouse event
 */
PXN8.mousePointToElementPoint = function(mx,my)
{
    var _ = PXN8.dom;
    var result = {};
    var canvas = _.id("pxn8_canvas");
    var imageBounds = _.eb(canvas);
    var scrolledPoint = PXN8.scrolledPoint(mx,my);
    result.x = Math.round((scrolledPoint.x - imageBounds.x)/PXN8.zoom.value());
    result.y = Math.round((scrolledPoint.y - imageBounds.y)/PXN8.zoom.value());
    
    if (canvas.style.borderWidth){
        
        var borderWidth = parseInt(canvas.style.borderWidth);
        result.x -= borderWidth;
        result.y -= borderWidth;
        if (result.x < 0){
            result.x = 0;
        }
        if (result.y < 0){
            result.y = 0;
        }
    }
    return result;
};

    
/***************************************************************************

PXN8.objectToString()
=====================
Converts a given javascript object to a string which can be evaluated as a JSON
expression.

Parameters
----------
* object : The object to be converted into a string.

Returns
-------
A string which can later be evaluated as a JSON expression.
Boolean literals (*true* and *false*) are converted to strings.

Examples
--------

    var myObject = {
                    name: "Walter Higgins",
                    contacts: ["John Doe", "K DeLong"],
                    available: false
                    };

    var myObjectAsString = PXN8.objectToString(myObject);

    // myObjectAsString = '{"name":"Walter Higgins", "contacts":["John Doe","K DeLong"],"available":"false"}';
                            

***/
PXN8.objectToString = function(obj)
{
    var s = "";

    var propToString = function(prop){return "\"" + prop + "\":";};

    var operationAlwaysFirst = function(a,b){
        if (a == "operation"){ return -1;} 
        if (b == "operation"){ return 1;} 
        return a > b ? 1 : b > a ? -1: 0;
    };
    
    var types = {array : {s:"[",e:"]", 
                          indexer: function(o){ var result = new Array(); for (var i =0;i < o.length;i++){result.push(i);}return result;}, 
                          pusher: function(array,o,i){array.push(o);}},
                 object: {s:"{",e:"}", 
                          indexer: function(o){ var result = new Array(); for (var i in o){ if (typeof o[i] != "function"){ result.push(i);}}return result.sort(operationAlwaysFirst);},
                          pusher: function(array,o,i){array.push(propToString(i) + o);}}
    };
    
    var type = "object";
    
    if (PXN8.isArray(obj)){
        type = "array";
    }
    
    s = types[type].s;
    
    var props = new Array();

    var pusher = types[type].pusher;
 
    var indexes = types[type].indexer(obj);
     
    for (var j = 0;j < indexes.length; j++){
        var i = indexes[j];
        if (typeof obj[i] == "function"){
            continue;
        }
        if (typeof obj[i] == "string"){
            pusher(props,"\"" +  obj[i] + "\"",i);
        }else if (typeof obj[i] == "object"){
            pusher(props, PXN8.objectToString(obj[i]),i);
        }else if (typeof obj[i] == "boolean"){
            pusher(props, "\"" + obj[i] + "\"",i);
        }else{
            pusher(props, obj[i],i);
        }
    }
    
    for (var i = 0;i < props.length; i++){
        s = s + props[i];
        if (i < props.length-1){
            s += ",";
        }
    }
    s += types[type].e;
        
    return s;
};

/**
 * Is an object an Array ?
 */
PXN8.isArray = function(o)
{
	return (o && typeof o == 'object') && o.constructor == Array;
};

/**
 * Return a random hexadecimal value in the range 0 - 65535 (0000 - FFFF)
 */
PXN8.randomHex = function()
{
    return (Math.round(Math.random()*65535)).toString(16)
};

PXN8.getImageBuffer = function()
{
    var _ = PXN8.dom;
    
    var buffer = _.id("pxn8_buffer");
    if (!buffer){
        buffer = _.ce("div",{id: "pxn8_buffer"});
        _.ac(document.body,buffer);
        buffer.style.width = "1px";
        buffer.style.height = "1px";
        buffer.style.overflow = "hidden";
    }
    return buffer;
};

/**
 * Replaces the current editing image with a new one
 */
PXN8.replaceImage = function(imageurl)
{
    var _ = PXN8.dom;

    var buffer = PXN8.getImageBuffer();
    
    // clear the buffer
    _.cl(buffer);

    //
    // create a new image element with an id of 'pxn8_buffered_image'
    //
    var theImage = _.ce("img",{id: "pxn8_buffered_image"});

    //
    // add the image to the buffer
    //
    _.ac(buffer,theImage);
    
    //
    // wph 20070630 : tell the user that the photo is loading
    // there is no visual clue now because the photo is first loaded into a
    // non-visible buffer.
    //
    var timer = _.id("pxn8_timer");
    if (timer){
        timer.style.display = "block";
        _.ac(_.cl(timer),_.tx("Loading photo. Please wait..."));
    }

    //
    // add the onload listener *BEFORE* setting the source 
    // so that the listener will get notified in IE.
    //
    PXN8.event.addListener(theImage,"load",PXN8.imageLoadNotifier);

    //
    // set the image's src attribute
    //
    theImage.src = imageurl;

    PXN8.show.size();


};

/*
 * Called when the AJAX request has returned
 */
PXN8.imageUpdateDone = function (jsonResponse) 
{
    var _ = PXN8.dom;

    var targetDiv = _.id("pxn8_image_container");
    PXN8.response = jsonResponse;

    if (jsonResponse && jsonResponse.status == "OK"){
        //
        // store the entire response object in the list of responses
        //
        PXN8.responses[PXN8.opNumber] = jsonResponse;
        //
        // wph 20060513: Workaround for IE's over-aggressive
        // image caching.
        // see IE bugs # 4
        // http://www.sourcelabs.com/blogs/ajb/2006/04/rocky_shoals_of_ajax_developme.html
	     //
        if (document.all){
            //
            // wph 20070226 : The passed in JSON response may have been
            // cached. If it was then don't force the image to reload.
            //
            /*
             * wph 20070226 : I can no longer see a need for this 
             * as the PXN8.replaceOnSave flag should be set to true
             * if the image is to be replaced with a new one.
             * This workaround may have been needed during development
             * but should not be required for production as the server-side
             * Pixenate code ensures each image has a unique ID.
             * (the case of between session changes is covered by the use
             *  of the PXN8.replaceOnSave flage - see PXN8.initialize()'s use of the
             *  'random' attribute in the first 'fetch' operation).
             *
             if (typeof jsonResponse["cached"] == "undefined"){
             jsonResponse.image += "?rnd=" + PXN8.randomHex();
             jsonResponse["cached"] = true;
             }
            */
        }
        //
        // prepend the PXN8 root path to the returned path
        // 
        var newImageSrc = PXN8.server + PXN8.root + "/" + jsonResponse.image;
        //
        // delete the old pxn8_image element and add a new pxn8_image element
        //
        PXN8.replaceImage(newImageSrc);
    }else{
        var status = PXN8.response;
        if (PXN8.response && typeof PXN8.response == "object"){
            
            status = PXN8.response.status;
            alert("An error occurred while updating the image.\n" +
                  "status: " + status + "\n" +
                  "errorMessage: " + PXN8.response.errorMessage);
        }else{
            alert("An error occurred while updating the image.\n" +
                  "status:" + status );
        }
        
        PXN8.listener.notify(PXN8.ON_IMAGE_ERROR);
        /**
         * wph 20070530 : Set PXN8.updating = false so that other tasks can be performed
         */
        PXN8.updating = false;
        // decrement the PXN8.opNumber variable !!!
        PXN8.opNumber--;

        //
        // hide the timer !!!
        //
        var timer = _.id("pxn8_timer");
        if (timer){
            timer.style.display = "none";
        }
    }

    PXN8.log.append(jsonResponse);

    //
    // mark as done
    //
    // wph 20070223: What if the image is large, takes a while to load but the user clicks
    // undo while the new image is loading ?
    //  Imagine the following scenario...
    //  user uploads a large image - PXN8.opNumber is 0
    //  user rotates the image - PXN8.opNumber is 1
    //  the server completes the rotate op and returns a JSON response pointing to the new image
    //  pixenate starts loading the new image 
    //  image is large so loads slowly - user clicks 'undo' - PXN8.opNumber is 0
    //  ...but unknown to the user, the new images' onload method will still get called !
    //     the onload method replaces PXN8.images[PXN8.opNumber] with the new image data
    //     (pxn8.opNumber has been reset to 0) so the image that the user sees and the image
    //     which the pixenate UNDO/REDO model sees are different. Since the undo/redo mechanism
    //     relies on the user NOT clicking 'undo' / 'redo' until after the new image is loaded.
    //     In order for the current undo/redo mechanism to work, the operation must be 
    //     LOCKED between firing the initial AJAX request and the image loading 
    //     so instead of setting PXN8.updating to false when the AJAX request returns
    //     ( as I do here ), PXN8.updating should be set to false ONLY WHEN THE NEW IMAGE
    //     HAS LOADED !
    // PXN8.updating = false;

    //
    // everything from here on used to be in PXN8.priv.postImageLoad()
    //
    var theImage = _.id("pxn8_image");
    theImage.onerror = function(){
        alert(PXN8.strings.IMAGE_ON_ERROR1 + theImage.src + PXN8.strings.IMAGE_ON_ERROR2);
        PXN8.listener.notify(PXN8.ON_IMAGE_ERROR);
    };
    
    //
    // IE Bug: If an image with the same URL has already been loaded
    // then the onload method is never called - need to explicitly call the
    // onloadFunc method so that listeners get notified etc.
    //
    /*
     * wph 20070508 : see PXN8.imageLoadHousekeeping !
     *
     *
    if (PXN8.imagesBySrc[theImage.src]){
        onloadFunc();
    }else{
        theImage.onload = onloadFunc;
    }
    
    PXN8.show.zoom();
    */
};

/* ============================================================================
 *
 * FUNCTIONS TO DISPLAY IMAGE INFORMATION
 */
PXN8.show = {};

/**
 * display selection info 
 */
PXN8.show.selection = function()
{
    var _ = PXN8.dom;
    
    var selectionField = _.id("pxn8_selection_size");
    if (selectionField){
        var text = "N/A";
        if (PXN8.ex - PXN8.sx > 0){
            text = (PXN8.ex-PXN8.sx) + "," + (PXN8.ey-PXN8.sy);
        }
        _.ac(_.cl(selectionField),_.tx(text));
    }
};

/**
 * display position info 
 */
PXN8.show.position = function()
{
    var _ = PXN8.dom;
    
    var posInfo = _.id("pxn8_mouse_pos");
    if (posInfo){
        var text = PXN8.position.x + "," + PXN8.position.y;
        _.ac(_.cl(posInfo),_.tx(text));
    }
};

/**
 * display position info 
 */
PXN8.show.zoom = function()
{
    var _ = PXN8.dom;
    
    var zoomInfo = _.id("pxn8_zoom");
    if (zoomInfo){
        var text = Math.round((PXN8.zoom.value() * 100)) + "%";
        _.ac(_.cl(zoomInfo),_.tx(text));
    }
};

/**
 * display size info 
 */
PXN8.show.size = function ()
{
    var _ = PXN8.dom;
    var sizeInfo = _.id("pxn8_image_size");
    if (sizeInfo){
        var text = PXN8.image.width + "x" + PXN8.image.height;
        _.ac(_.cl(sizeInfo),_.tx(text));
    }
};

/**
 * Display a soft alert that disappears after a short time
 */
PXN8.show.alert = function (message,duration)
{
    var _ = PXN8.dom;

    duration = duration || 1000;
    
    var warning = _.id("pxn8_warning");
    if (!warning){
        warning = _.ce("div",{id: "pxn8_warning",className: "warning"});
        
    }
    
    warning.style.width  = (PXN8.image.width>200?PXN8.image.width:200) + "px";

    _.ac(_.cl(warning),_.tx(message));
    _.ac(_.id("pxn8_canvas"),warning);
    
    setTimeout("PXN8.fade.init();PXN8.fade.fadeout('pxn8_warning',true);",duration);
};



/* ============================================================================
 *
 * Fade functions - make a HTML element fade in and out
 */

PXN8.fade = {
	values: [0.99,0.85, 0.70, 0.55, 0.40, 0.25, 0.10, 0],
	times:      [75, 75,  75,  75,  75,  75,  75,  75],
	i: 0,
	stopfadeout: false
};

PXN8.fade.init = function(){ var self = PXN8.fade; self.i =0; self.stopfadeout = false;};

PXN8.fade.cancel = function(){ var self = PXN8.fade; self.stopfadeout = true; };

PXN8.fade.fadeout = function(eltid,destroyOnFade)
{
    var _ = PXN8.dom;
    var self = PXN8.fade;
    
    if (self.stopfadeout){
        return;
    }
    _.opacity(eltid,self.values[self.i]);
    if (self.i < self.values.length -1 ){
        self.i++;
        setTimeout("PXN8.fade.fadeout('" + eltid + "'," + destroyOnFade + ");",self.times[self.i]);
    }else{
        if (destroyOnFade){
            var node = _.id(eltid);
            // it's quite possible that the element has already been destroyed !
            if (!node){
                return;
            }else{
                var parent = node.parentNode;
                parent.removeChild(node);
            }
        }
    }
};

PXN8.fade.fadein = function(eltid)
{
    var _ = PXN8.dom;
    var self = PXN8.fade;
    try{
        if (self.i >= self.values.length){
            self.i = self.values.length - 1;
        }
        _.opacity(eltid,self.values[self.i]);
        if (self.i > 0){
            self.i--;
            setTimeout("PXN8.fade.fadein('" + eltid + "');",self.times[self.i]);
        }
    }catch(e){
        alert(e.message);
    }
};

/**
 * 
 */
PXN8.offsets = [];


/**
 * Add a new operation to the PXN8.history !
 * (called via PXN8.tools.updateImage() - do not call directly !).
 */
PXN8.addOperations = function(operations)
{
    var self = PXN8;

    //
    // must call getUncompressedImage() _BEFORE_ opNumber is incremented !
    // to get the corrected cachedImage value.
    //
    var cachedImage = self.getUncompressedImage();
    
    // increment opNumber just once
    self.opNumber++;
    
    self.offsets[self.opNumber] = operations.length;

    var lastIndex = 0;
    for (var i = 0;i < self.opNumber; i++){
        lastIndex += self.offsets[i];
    }

    // add each operation to the history member
    for (var i = 0;i < operations.length; i++){
        self.history[lastIndex + i] = operations[i];
    }

    self.maxOpNumber = self.opNumber;

    var script = PXN8.getScript();
    
    self.prepareForSubmit();

    self.ajax.submitScript(script,self.imageUpdateDone);
    
};
/**
 * wph 20070105
 * Adjust the current selection to snap to the aspect ratio if one is enforced.
 */
PXN8.snapToAspectRatio = function()
{
    var sel = PXN8.getSelection();
    //
    // say ratio is 5x3 and current selection is 400x280
    // the selection should be shrunk to 400x240
    // new height = (400/5) * 3;
    //
    // say ratio is 5x3 and current selection is 280x500
    // the selection should be shrunk to 280x168
    // new height = (280/5) * 3;
    //
    // say ratio is 3x5 and current selectin is 400x280
    // the selection should be shrunk to 168x280
    // new width = (280/5) * 3;
    //
    // etc...
    //
    if (PXN8.aspectRatio.width != 0){
        //
        // an aspect ratio is enforced
        //
        if (PXN8.aspectRatio.width > PXN8.aspectRatio.height){
            sel.height = Math.round((sel.width / PXN8.aspectRatio.width ) * PXN8.aspectRatio.height);
        }else{
            sel.width = Math.round((sel.height / PXN8.aspectRatio.height) * PXN8.aspectRatio.width);
        }
        PXN8.select(sel.left,sel.top,sel.width,sel.height);
    }
};

/***************************************************************************

SECTION: Zooming : Variables and Related Functions
==================================================
The following variables and functions are used for zooming in and out.
Zooming (or magnification) only changes the appearance of the photo in the 
browser and does not change the photo's real size.

***/


/**************************************************************************

PXN8.zoom
=========
PXN8.zoom is a namespace used by all of the zoom-related variables and functions.

***/
PXN8.zoom = {};


/***************************************************************************

PXN8.zoom.values
================
Users can zoom in and out of a photo by cycling through an array of predefined zoom values.
PXN8.zoom.values specifies the levels of magnification a user can cycle through.

Type
----
Array

Default Value
-------------
    [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 2, 3, 4 ];

Related
-------
PXN8.zoom.index
***/ 

PXN8.zoom.values = [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 2, 3, 4 ];

/****************************************************************************

PXN8.zoom.index
===============
PXN8.zoom.index is an index into the (zero-based) array of PXN8.zoom.values.

Type
----
number

Default Value
-------------
    3
***/
PXN8.zoom.index = 3;

PXN8.zoom.zoomedBy = PXN8.zoom.values[PXN8.zoom.index];

/***************************************************************************

PXN8.zoom.value()
=================
Get the current magnification value in use. This is expressed as a float. e.g. 200% magnification 
returns a value of 2.0

Returns
-------
A float value - the current magnification factor.

Related
-------
PXN8.zoom.canZoomIn PXN8.zoom.canZoomOut PXN8.zoom.zoomIn PXN8.zoom.zoomOut PXN8.zoom.zoomByIndex PXN8.zoom.toSize

***/
PXN8.zoom.value = function()
{
    return PXN8.zoom.zoomedBy;
};
/***************************************************************************

PXN8.zoom.canZoomIn()
=====================
Indicates whether or not the image magnification can be increased any further.

Returns
-------
true or false.

Related
-------
PXN8.zoom.canZoomOut PXN8.zoom.zoomByIndex PXN8.zoom.zoomIn PXN8.zoom.zoomOut PXN8.zoom.value PXN8.zoom.toSize

***/
PXN8.zoom.canZoomIn = function(){
    var self = PXN8.zoom;
    return self.zoomedBy < self.values[self.values.length-1];
};

/***************************************************************************

PXN8.zoom.canZoomOut()
======================
Indicates whether or not the image magnification can be decreased any further.

Returns
-------
true or false.

Related
-------
PXN8.zoom.canZoomIn PXN8.zoom.zoomByIndex PXN8.zoom.zoomIn PXN8.zoom.zoomOut PXN8.zoom.value PXN8.zoom.toSize

***/
PXN8.zoom.canZoomOut = function(){
    var self = PXN8.zoom;
    return self.zoomedBy > self.values[0];
};

/***************************************************************************

PXN8.zoom.zoomByIndex()
=======================
Zoom the photo to a magnification level at the specified index (see the 
PXN8.zoom.values array for a list of magnification levels.

Parameters
----------
 
* index : The index into the PXN8.zoom.values array. E.g. PXN8.zoom.values has this value

     [0.25, 0.5, 0.75, 1.0, 1.5, 2]

The PXN8.zoom.zoomByIndex(2) will zoom the image to 75% (0.75 is the value a PXN8.zoom.values[2]).

Related
-------
PXN8.zoom.canZoomIn PXN8.zoom.canZoomOut PXN8.zoom.zoomIn PXN8.zoom.zoomOut PXN8.zoom.value PXN8.zoom.toSize

***/
PXN8.zoom.zoomByIndex = function(index)
{
    PXN8.zoom.setIndex(index);
};

PXN8.zoom.setIndex = function(i)
{
    var self = PXN8.zoom;
    self.index = i;
    self.setValue(self.values[i]);
};
PXN8.zoom.setValue = function(magnification)
{
    /**
     * wph 20070516 - zoom on a large image which hasn't yet fully loaded
     * will make the image disappear.
     */
    if (!PXN8.ready){
        return;
    }
    var self = PXN8.zoom;
    self.zoomedBy = magnification;

    //
    // update the width and height of the image
    //
    var theImg = document.getElementById("pxn8_image");
    theImg.width = PXN8.image.width * magnification;
    theImg.height = PXN8.image.height * magnification;

    PXN8.selectArea();
    PXN8.listener.notify(PXN8.ON_ZOOM_CHANGE);
};

    
    

/***************************************************************************

PXN8.zoom.zoomIn()
==================
Zoom in (Increase the magnification level) so the photo appears bigger.
The amount by which the magnification level increases depends on the values in the
*PXN8.zoom.values* array.

Related
-------
PXN8.zoom.canZoomIn PXN8.zoom.canZoomOut PXN8.zoom.zoomByIndex PXN8.zoom.zoomOut PXN8.zoom.value PXN8.zoom.toSize

***/
PXN8.zoom.zoomIn = function()
{
    var self = PXN8.zoom;
    
    /**
     * wph 20070516 - zoom on a large image which hasn't yet fully loaded
     * will make the image disappear.
     */
    if (!PXN8.ready){
        return;
    }
    
    if (self.canZoomIn())
    {
        for (var i = 0; i < self.values.length;i++){
            if (self.values[i] > self.zoomedBy){
                self.setIndex(i);
                break;
            }
        }

    }else{
        PXN8.show.alert(PXN8.strings.NO_MORE_ZOOMIN,500);
    }
    // return false in case this is called from a link
    return false; 
};

/***************************************************************************

PXN8.zoom.zoomOut()
===================
Zoom out (Decrease the magnification level) so the photo appears smaller.
The amount by which the magnification level decreases depends on the values in the
*PXN8.zoom.values* array.

Related
-------
PXN8.zoom.canZoomIn PXN8.zoom.canZoomOut PXN8.zoom.zoomByIndex PXN8.zoom.zoomIn PXN8.zoom.value PXN8.zoom.toSize

***/
PXN8.zoom.zoomOut = function()
{
    var self = PXN8.zoom;
    /**
     * wph 20070516 - zoom on a large image which hasn't yet fully loaded
     * will make the image disappear.
     */
    if (!PXN8.ready){
        return;
    }
    if (self.canZoomOut()){
        for (var i = self.values.length-1; i >= 0; i--){
            if (self.values[i] < self.zoomedBy){
                self.setIndex(i);
                break;
            }
        }
    }else{
        PXN8.show.alert(PXN8.strings.NO_MORE_ZOOMOUT,500);
    }
    return false;
};

/***************************************************************************

PXN8.zoom.toSize()
==================
Zoom the image to a fixed width and height.

Parameters
----------

* width : The width to zoom to.
* height: The height to zoom to.

Related
-------
PXN8.zoom.canZoomIn PXN8.zoom.canZoomOut PXN8.zoom.zoomByIndex PXN8.zoom.zoomIn PXN8.zoom.value PXN8.zoom.toSize

***/
PXN8.zoom.toSize = function(width, height)
{
    var hr = width / PXN8.image.width ;
    var vr = height / PXN8.image.height ;

    PXN8.zoom.setValue(Math.min(vr,hr));
    
    return false;
};

PXN8.zoom.zoomByValue = function(magnification)
{
    PXN8.zoom.setValue(magnification);
};

    
/* ============================================================================
 *
 * PRIVATE FUNCTIONS and members internal to PXN8 only - do not call from client code
 */

/**
 * history stores all session operations
 */
PXN8.history =  [];

/**
 * An array of the response images returned from the server
 * This array contains relative file paths. 
 * It is updated in the  imageUpdateDone() function.
 */
PXN8.responses =  [];

/**
 * images stores a list of all images indexed by opNumber
 * (used by PXN8.tools.history)
 */
PXN8.images =  [];

/**
 * A flag which is set when the image has fully loaded
 */
PXN8.ready = false;

/*
 * The current image - it's width; height and location (URL)
 */
PXN8.image =   { 
    width: 0, 
    height: 0, 
    location: ""
};

PXN8.priv = {
};

PXN8.priv.addImageToHistory = function(imageLocation)
{
    PXN8.log.append(" addImageToHistory (" + imageLocation + " " + PXN8.image.width + "," + PXN8.image.height + ")");

    var item = {"location": imageLocation,
                "width": PXN8.image.width,
                "height": PXN8.image.height
    };
            
    PXN8.images[PXN8.opNumber] = item;

    // 
    // wph 20070223 : see comments in imageUpdateDone()
    //
    PXN8.updating = false;

    for (var i = 0; i <= PXN8.maxOpNumber; i++){
        var item = PXN8.images[i];
        if (item){
            PXN8.log.append("-- [" +i+ "] " + item.location + " " + item.width + "," + item.height);
        }else{
            PXN8.log.append("-- [" +i+ "] " + item);
        }
    }
    PXN8.log.append("---------");
};

/**
 * Create the selection area if it's not already defined.
 */
PXN8.priv.createSelectionRect = function()
{
    var _ = PXN8.dom;
    var selectRect = _.id("pxn8_select_rect");
    if (!selectRect){
        var canvas = _.id("pxn8_canvas");
        selectRect = _.ac(canvas, _.ce("div", {id: "pxn8_select_rect"}));
        selectRect.style.backgroundColor = "white";
        _.opacity(selectRect,0);
        selectRect.style.cursor = "move";
        selectRect.style.borderWidth  = "1px";
        selectRect.style.borderColor = "red";
        selectRect.style.borderStyle = "dotted";
        selectRect.style.position = "absolute";
        selectRect.style.zIndex = 1;
        selectRect.style.fontSize = "0px";
        selectRect.style.display = "block";
        selectRect.style.width = "0px";
        selectRect.style.height = "0px";
    }
    selectRect.onmousedown = function(event){ 
        if (!event) event = window.event;
        PXN8.drag.begin(selectRect,event,
                        PXN8.drag.moveSelectionBoxHandler,
                        PXN8.drag.upSelectionBoxHandler);
    };
    return selectRect;
};

/**
 * A private function called when the image has loaded.
 * This function in turn calls all of the PXN8.ON_IMAGE_LOAD listeners
 */
PXN8.imageLoadNotifier = function()
{
    PXN8.listener.notify(PXN8.ON_IMAGE_LOAD);
};

/*
 * Sets up the mouse handlers for the canvas area
 * Some tools/operations might modify the canvas mouse behaviour
 * If they do so then they should call this method when the tool's
 * work is done or cancelled.
 */
PXN8.initializeCanvas = function()
{
    var _ = PXN8.dom;

    var canvas = _.id("pxn8_canvas");

    canvas.onmousemove = function (event){ 
        if (!event) event = window.event;
	     var cursorPos = _.cursorPos(event);
        var imagePoint = PXN8.mousePointToElementPoint(cursorPos.x, cursorPos.y);
        PXN8.position.x = imagePoint.x;
        PXN8.position.y = imagePoint.y;
        PXN8.show.position();
        return true;
    };

    canvas.onmouseout = function (event){ 
        if (!event) event = window.event;
        PXN8.position.x = "-";
        PXN8.position.y = "-";
        PXN8.show.position();
    };
    canvas.onmousedown = function (event){
        if (!event) event = window.event;
        PXN8.drag.begin(canvas,
                        event,
                        PXN8.drag.moveCanvasHandler,
                        PXN8.drag.upCanvasHandler);
    };
    canvas.ondrag = function(){ 
        return false;
    };

    var computedCanvasStyle = _.computedStyle("pxn8_canvas");

    var canvasPosition = null;
    
    if (computedCanvasStyle.getPropertyValue){
        canvasPosition = computedCanvasStyle.getPropertyValue("position");
    }else{
        if (!computedCanvasStyle.position){
            // position may not be available if 
            // computedStyle returns the inline style (on safari).
            //
            canvasPosition = "static";
        }else{
            canvasPosition = computedCanvasStyle.position;
        }
    }
    
    if (!canvasPosition || canvasPosition == "static"){
        // default the canvas position to relative
        canvas.style.position = "relative";
        canvas.style.top = "0px";
        canvas.style.left  = "0px";
    }
    //
    // the canvas should wrap tightly around the image
    // so that the canvas doesn't extend beyond the image,
    // set it's float css property if it hasn't already been set.
    //
    var floatProperty = "cssFloat";
    if (document.all){
        floatProperty = "styleFloat";
    }
    var floatValue = computedCanvasStyle[floatProperty];
    
    if (!floatValue || floatValue == "none"){
        canvas.style[floatProperty] = "left";
    }

    return canvas;
};

/* 
 * END OF DECLARATIONS SECTION
 * ============================================================================
 */

PXN8.listener.add(PXN8.ON_IMAGE_CHANGE, PXN8.show.zoom);
PXN8.listener.add(PXN8.ON_ZOOM_CHANGE, PXN8.show.zoom);

/* ============================================================================
 *
 * (c) Copyright SXOOP Technologies Ltd. 2005-2007
 * All rights reserved.
 *
 * This file contains code which handles display of the Pixenate Toolbar.
 *
 */

var PXN8 = PXN8 || {};

PXN8.toolbar = {};

/**
 * You can override this value in your html
 */
PXN8.toolbar.crop_options = ["4x6","5x8"];


/**
 * Hide the dropdown menu
 */
PXN8.toolbar.hidemenu = function(e)
{

    if (!e) var e = window.event;
    var tg = (window.event) ? e.srcElement : e.target;
    if (tg.nodeName != 'DIV') return;
    var reltg = (e.relatedTarget) ? e.relatedTarget : e.toElement;
    while (reltg != tg && reltg.nodeName != 'BODY'){
        reltg = reltg.parentNode;
    }
    if (reltg == tg) return;
        
    tg.style.display = "none";
};

/**
 * OnToolClick handles the special case of a toolbar button which 
 * has a default action or displays a dropdown menu of operations if the 
 * dropdown arrow is clicked.
 * menuDiv : the dropdown menu's div
 * button_offset: a numeric offset for where the dropdown arrow is
 * menuMap: A hash of menu text to functions
 * default_func: The default function to be called if the dropdown arrow
 * isn't clicked.
 */
PXN8.toolbar.ontoolclick = function(event, menuDiv, button_offset, menuMap, default_func)
{
    var dom = PXN8.dom;
    if (!event){
        event = window.event;
    }
    var button = (window.event) ? event.srcElement : event.target;
        
    menuDiv.onmouseout = function(event){
        PXN8.toolbar.hidemenu(event);
    };

    if (menuDiv.style.display == "block"){
        menuDiv.style.display = "none";
        return;
    }

    /**
     * Hide all other dropdowns
     */
    var dropdowns = PXN8.dom.clz("pxn8_toolbar_dropdown");
    for (var i = 0; i < dropdowns.length; i++){
        var dropdown = dropdowns[i];
        dropdown.style.display = "none";
    }
        
            
    var pos = dom.eb(button);
    var ox = event.clientX - pos.x ;
    var oy = event.clientY - pos.y ;
        

    if (ox > button_offset){
        dom.cl(menuDiv);

        for (var i in menuMap){
            var link = dom.ce("a",{href: "javascript:void(0);",
                                   className: "pxn8_toolbar_option",
                                   onclick : menuMap[i].onclick});
            if (menuMap[i].image){
                var linkImage = dom.ce("img", {border: 0, src: PXN8.server + PXN8.root + "/" +menuMap[i].image});
                dom.ac(link,linkImage);
            }
                
            dom.ac(link,dom.tx(i));
            dom.ac(menuDiv,link);
        }
        menuDiv.style.display = "block";
        menuDiv.style.top = pos.y + pos.height + 4 + "px";
        menuDiv.style.left = (pos.x - 4) + "px";
            
    }else{
        default_func();
    }
    return false;

};
/**
 * Toolbar menu definitions go here
 */
PXN8.toolbar.menu = {
    crop: {},
    rotate: {},
    instantFix: {}
};

PXN8.toolbar.menu.rotate = {
    /**
     * Populate gets called by PXN8.toolbar.draw when the page first loads
     */ 
    populate: function(){
        
    }
};



PXN8.toolbar.buttons = {};

PXN8.toolbar.buttons.zoomin = {
    onclick: function(){ 
        PXN8.zoom.zoomIn();return false;
    },
    image: "/images/icons/magnifier_zoom_in.gif",
    tip: "Zoom In"
};

PXN8.toolbar.buttons.zoomout = {
    onclick: function(){PXN8.zoom.zoomOut();return false;},
    image: "/images/icons/magnifier_zoom_out.gif",
    tip: "Zoom Out"
};

PXN8.toolbar.buttons.rotate = {
    onclick: function(event){
        var dropdown = document.getElementById("pxn8_toolbar_rotate");
        var menuContents = {
            "Clockwise": {
                onclick: function(){PXN8.tools.rotate({angle: 90});dropdown.style.display = "none";return false;},
                image: "/images/icons/rotate_clockwise.gif"
            },
                
            "Anti-Clockwise": {
                onclick: function(){PXN8.tools.rotate({angle: 270});dropdown.style.display = "none";return false;},
                image: "/images/icons/rotate_anticlockwise.gif"
            },
            "Flip Vertically": {
                onclick: function(){PXN8.tools.rotate({flipvt: "true"});dropdown.style.display = "none";return false;},
                image: "/images/icons/shape_flip_vertical.gif"
                
            },
            "Flip Horizontally": {
                onclick: function(){PXN8.tools.rotate({fliphz: "true"});dropdown.style.display = "none";return false;},
                image: "/images/icons/shape_flip_horizontal.gif"
            }
            
        };
        PXN8.toolbar.ontoolclick(event,dropdown,40,menuContents,function(){PXN8.tools.rotate({angle:90});});
        return false;
    }, 
    image: "/images/icons/rotate.gif",
    tip: "Rotate the photo by 90 degrees clockwise"
};

PXN8.toolbar.buttons.add_text = {
    onclick: function(event){},
    image: "/images/icons/add_text.gif",
    tip: "Add Text to photo"
};

PXN8.toolbar.buttons.normalize = {
    onclick: function(){PXN8.tools.normalize();return false;}, 
    image: "/images/icons/normalize.gif",
    tip: "Gives better color balance"
};

PXN8.toolbar.buttons.enhance = {
    onclick: function(event){PXN8.tools.enhance();return false;}, 
    image: "/images/icons/enhance.gif",
    tip: "Smooths facial lines"
};

PXN8.toolbar.buttons.save = {
    onclick: function(event){return PXN8.save.toServer();}, 
    image: "/images/icons/save.gif",
    tip: "Save image to server"
};

PXN8.toolbar.buttons.instantFix = {
    onclick: function(event){

        var dropdown = document.getElementById("pxn8_toolbar_fix");
        var fixes = {
            "Enhance":   {onclick: function(){PXN8.tools.enhance();  dropdown.style.display = "none";return false;}},
            "Normalize": {onclick: function(){PXN8.tools.normalize();dropdown.style.display = "none";return false;}}
        };
        PXN8.toolbar.ontoolclick(event,dropdown,48,fixes,PXN8.tools.instantFix);
        
        return false;
    }, 
    image: "/images/icons/instant_fix.gif",
    tip: "A quick fix solution - gives better color balance and smooths lines"
};

PXN8.toolbar.buttons.crop = {
    onclick: function(event){
        
        var dropdown = document.getElementById("pxn8_toolbar_crop");
        
        var callback = function(opt){
            return function(){
                PXN8.selectByRatio(opt);
                document.getElementById("pxn8_toolbar_crop").style.display = "none";
                return false;
            };
        };
        
        var menuContents = {};
        
        for (var i = 0;i < PXN8.toolbar.crop_options.length; i++){
            var option = PXN8.toolbar.crop_options[i];
            menuContents[option] = {onclick: callback(option)};
        }
        
        PXN8.toolbar.ontoolclick(event,dropdown,40,menuContents,function(){
            var selection = PXN8.getSelection();
            
            if (selection.width > 0){
                PXN8.tools.crop(selection);
            }else{
                PXN8.show.alert("Select an area to crop");
            }
        });
        return false;
    },
    image: "/images/icons/cut_red.gif",
    tip: "Crop the image"
};

PXN8.toolbar.buttons.fillflash = {
    onclick: function(){PXN8.tools.fill_flash();return false;},
    image: "/images/icons/lightning_add.gif",
    tip: "Add Fill-Flash to brighten the image"
    
};

PXN8.toolbar.buttons.undo = {
    onclick: function(){PXN8.tools.undo();return false;},
    image: "/images/icons/undo.gif",
    tip: "Undo the last operation"
    
};

PXN8.toolbar.buttons.redo = {
    onclick: function(){PXN8.tools.redo();return false;},
    image: "/images/icons/redo.gif",
    tip: "Redo the last operation"
};

PXN8.toolbar.buttons.undoall = {
    onclick: function(){PXN8.tools.undoall();return false;}, 
    image: "/images/icons/undo_all.gif",
    tip: "Undo all operations"
};

PXN8.toolbar.buttons.redoall = {
    onclick: function(){PXN8.tools.redoall();return false;}, 
    image: "/images/icons/redo_all.gif",
    tip: "Redo all operations"
};

PXN8.toolbar.buttons.selectall = {
    onclick: PXN8.selectAll,
    image: "/images/icons/selectall.gif",
    tip: "Select entire photo"
};

PXN8.toolbar.buttons.unselect = {
    onclick: PXN8.unselect,
    image: "/images/icons/unselect.gif",
    tip: "Select entire photo"
};


/**
 * -- function:    PXN8.toolbar.draw
 * -- description: Draw the toolbar
 * -- param: buttons. An array of strings. Can be any of the following... 
 * --   "undo","redo","undoall", "redoall", "unselect", "selectall", "fillflash", 
 * --   "crop", "rotate", "instantFix", "enhance", "normalize", "zoomout","zoomin"
 */
PXN8.toolbar.draw = function(buttons){

    var dom = PXN8.dom;
    
    if (!buttons){
        buttons = new Array();
        for (var i in PXN8.toolbar.buttons){
            buttons.push(i);
        }
    }
    
    document.writeln("<table cellspacing='0' cellpadding='0'><tbody><tr id='pxn8_toolbar_table'></tr></tbody></table>");

    var dropdowns = ["pxn8_toolbar_crop", "pxn8_toolbar_fix", "pxn8_toolbar_rotate"];
    
    for (var i = 0;i < dropdowns.length; i++){
        document.writeln("<div id='" + dropdowns[i] + "' class='pxn8_toolbar_dropdown' style='display:none;'></div>");
    }
    /**
     * wph 20060704: Need to move the drop-down menus up to the 
     * document.body because the position will break inside a 
     * relative div.
     * Create a closure that will move the dropdown menus to the body
     * when executed.
     */
    function moveMenusToBody(){
        for (var i = 0;i < dropdowns.length; i++){
            var menuElement = document.getElementById(dropdowns[i]);
            var menuParent = menuElement.parentNode;
            menuParent.removeChild(menuElement);
            document.body.appendChild(menuElement);
        }
    };
    /**
     * Delay running the closure until the document has loaded.
     */
    PXN8.dom.addLoadEvent(moveMenusToBody);
    
	 var toolbar = dom.id('pxn8_toolbar_table');

    for (var h =0; h < buttons.length; h++)
    {
        var i= buttons[h];

        var widgetModel = PXN8.toolbar.buttons[i];
        
        var cell = dom.ac(toolbar,dom.ce("td"));
        var widget = dom.ce("a",{className: "pxn8_toolbar_btn", 
                                 href: "javascript:void(0);",
                                 onclick: widgetModel.onclick,
                                 title: widgetModel.tip,
                                 onmousedown: function(){this.className = 'pxn8_toolbar_btndown';},
                                 onmouseup: function(){this.className = 'pxn8_toolbar_btn';},
                                 onmouseout: function(){this.className = 'pxn8_toolbar_btn';}
        });

        
        
	     var arrowLink = dom.ce("a",{href: "javascript:void(0);",
                                    onclick: function(event,element){
                                        widgetModel.arrowClicked = widgetModel.arrowClicked==true?false:true;
                                    }
        });
                                
        dom.ac(cell,widget);
        var widgetImage = dom.ce("img", {border: 0, 
                                         alt: widgetModel.tip, 
                                         src: PXN8.server + PXN8.root + widgetModel.image
        });
        dom.ac(widget,widgetImage);

    }

    /* dom.ac(dom.ac(dom.ac(toolbar,
                         dom.ce("td")),
                  dom.ce("a",{ href: "http:/" + "/pixenate.com/"})),
           dom.ce("img",{border: 0, 
                         id: "pxn8_poweredby", 
                         src: PXN8.server + PXN8.root + "/images/icons/powered_by_pxn8.gif"})); */
    
};
/* ============================================================================
 *
 * (c) Copyright SXOOP Technologies Ltd. 2005-2007
 * All rights reserved.
 *
 * This file contains code which handles event managment
 *
 */
var PXN8 = PXN8 || {};


/***************************************************************************

SECTION: DOM Event handling Wrapper functions
=============================================
The following section details some DOM-related event-handling functions used by 
Pixenate.

***/

PXN8.event = {};
PXN8.event._added = [];

/**************************************************************************

PXN8.event.addListener()
========================
A cross-browser way to add event listeners - works with Safari, Internet Explorer and Firefox.

Parameters
----------
* element : The element (or the ID of the element) to which the event listener will be added.
* eventType : The event type string e.g. "mouseup","mousedown","click","keypress" etc.
* eventHandler :  The function which will be called when the event fires.

Returns
-------
The callback function which was supplied as an argument.

Examples 
--------
    var onButtonClick = PXN8.event.addListener(document.getElement("myButton"),"click",function(event){
       event = event || window.event;
       alert("You clicked the button!");
    });

Related
-------
PXN8.event.removeListener()

***/
PXN8.event.addListener = function(el,eventstr,func){

    if (typeof el == 'string'){ el = PXN8.dom.id(el); }
    
    if (el.addEventListener){
        el.addEventListener(eventstr,func,true);
    }else if (el.attachEvent){
        el.attachEvent("on" + eventstr,func);
    }
    
    var record = {"element": el, "event": eventstr, "listener": func};
    PXN8.event._added.push(record);
    return func;
};

/*************************************************************************

PXN8.event.removeListener()
===========================
A cross-browser way to remove event listeners - works with Safari, Internet Explorer and Firefox. 

Parameters
----------
* element : The element (or the ID of the element) from which the event listener will be removed.
* eventType : The event type string e.g. "mouseup","mousedown","click","keypress" etc.
* eventHandler : The callback to be removed (must be a named function or a function reference - see PXN8.event.addListener)

Examples
--------
To remove a named function from the list of listeners for an event on a particular element...

    PXN8.event.removeListener(document.getElementById("myButton"),"click",onButtonClick);

Related
-------
PXN8.event.addListener()

***/
PXN8.event.removeListener = function(el,eventstr,func){

    if (typeof el == 'string'){ el = PXN8.dom.id(el);}
    
    if (func){
        PXN8.event._removeListener(el,eventstr,func);
    } else {
        // if no func is supplied then remove ALL listeners of this type 
        // from the element
        var original = PXN8.event._added;
        var removed = false;
        for (var i =0; i < original.length; i++){
            var record = original[i];
            if (record){
                if (record.eventstr == eventstr && record.element == el){
                    PXN8.event._removeListener(el,eventstr,record.listener);
                    original[i] = null;
                    removed = true;
                }
            }
        }
        if (!removed){ return ;}
        /**
         * Clear out any null elements from _added (not safe to do this in above loop).
         */
        var temp = [];
        for (var i = 0;i < original.length;i++){
            if (original[i] != null){
                temp.push(original[i]);
            }
        }
        PXN8.event._added = temp;
    }
};

PXN8.event._removeListener =  function(el,eventstr,func){

    if (typeof el == 'string'){ el = PXN8.dom.id(el); }
    
    if (el.removeEventListener){
        el.removeEventListener(eventstr,func,true);
    }else if (el.detachEvent){
        el.detachEvent("on" + eventstr,func);
    }
};

    
/**
 * PXN8.event.closure creates an event closure 
 * Parameters: object The object to be baked into the event handler
 * func - The event handler (a function)
 * The closure returned will take 4 parameters...
 * object: The object that has been baked in.
 * source: The HTML element that triggered the event
 * event: The event which triggered the function call.
 * caller: The closure itself - this will not be the same as the function passed into PXN8.event.closure.
 */
PXN8.event.closure = function(object,func){
    return function(event){
        event = event || window.event;
        var source = (window.event) ? event.srcElement : event.target;
        func(event,object,source,arguments.callee);
    };
};

/**
 * Creates an event handler where the source, and event are guaranteed to be present and correct
 * It does things like normalizing the event (removing IE & firefox discrepancies)
 */
PXN8.event.normalize = function(func){
    return function(event){
        event = event || window.event;
        var source = (window.event) ? event.srcElement : event.target;
        func(event,source,arguments.callee);
    };
};


/**
 * Bind event-handling behaviour to all elements of a particular class
 */
PXN8.behaviour = {
    
    bind: function(className,behaviourObject){
        var elements = PXN8.dom.clz(className);
        for (var i = 0;i < elements.length; i++)
        {
            for (var j in behaviourObject){
                PXN8.event.addListener(elements[i],j,behaviourObject[j]);
            }
        }
    }
};
/* ============================================================================
 *
 * (c) Copyright SXOOP Technologies Ltd. 2005-2007
 * All rights reserved.
 *
 * This file contains code to draw and manage the selection box
 */
var PXN8 = PXN8 || {};

/* ============================================================================ 
 *
 * Drag - related functions and members
 */

PXN8.drag = {
    dx: 0,
    dy: 0,
    beginDragX: 0,
    beginDragY: 0,
    
    /* used when dragging selection */
    osx: 0,
    osy: 0,
    ow: 0,
    oh: 0
};

PXN8.drag.begin = function (elementToDrag, event, moveHandler, upHandler)
{
    var _ = PXN8.dom;
    
    var elementBounds = _.eb(elementToDrag);

    var cursorPos = _.cursorPos(event);
    
    var scrolledPoint = PXN8.scrolledPoint(cursorPos.x,cursorPos.y);

    
    PXN8.drag.beginDragX = scrolledPoint.x;
    PXN8.drag.beginDragY = scrolledPoint.y;
    
    PXN8.drag.dx = scrolledPoint.x - elementBounds.x;
    PXN8.drag.dy = scrolledPoint.y - elementBounds.y;
    
    PXN8.drag.osx = PXN8.sx;
    PXN8.drag.osy = PXN8.sy;
    PXN8.drag.ow = PXN8.ex - PXN8.sx;
    PXN8.drag.oh = PXN8.ey - PXN8.sy;
    
    if (document.addEventListener){
        document.addEventListener("mousemove", moveHandler, true);
        document.addEventListener("mouseup", upHandler, true);
    }else if (document.attachEvent){
        document.attachEvent("onmousemove",moveHandler);
        document.attachEvent("onmouseup",upHandler);
    }
    if (event.stopPropogation) {
        event.stopPropogation();/* DOM Level 2 */
    }else{
        event.cancelBubble = true; /* IE */
    }
    
   
    if (event.preventDefault){
        event.preventDefault(); /* DOM Level 2 */
    }else {
        event.returnValue = false; /*  IE */
    }
};

PXN8.drag.moveCanvasHandler = function (event)
{
    var _ = PXN8.dom;
    
    if (!event) event = window.event; /* IE */

    var canvasBounds = _.eb("pxn8_canvas");
    
    var theImg = _.id("pxn8_image");

    var maxX = canvasBounds.x + theImg.width;
    var maxY = canvasBounds.y + theImg.height;
    
    var cursorPos = _.cursorPos(event);
    /*
     * prohibit move outside right and bottom
     */
    var scrolledPoint = PXN8.scrolledPoint(cursorPos.x, cursorPos.y);
    
    var x2 = scrolledPoint.x>maxX?maxX:scrolledPoint.x; 
    x2 = x2 < canvasBounds.x?canvasBounds.x:x2;
    var y2 = scrolledPoint.y>maxY?maxY:scrolledPoint.y;
    y2 = y2 < canvasBounds.y?canvasBounds.y:y2;

    var numerical = function(a,b){
        return a-b;
    };
    var xVals = [PXN8.drag.beginDragX-canvasBounds.x,x2-canvasBounds.x].sort(numerical);
    var yVals = [PXN8.drag.beginDragY-canvasBounds.y,y2-canvasBounds.y].sort(numerical);
    
    var pixelWidth = xVals[1] - xVals[0];
    var pixelHeight = yVals[1] - yVals[0];
    
    var width = Math.round(pixelWidth / PXN8.zoom.value());
    var height = Math.round(pixelHeight / PXN8.zoom.value());
    
    height = height > PXN8.image.height?PXN8.image.height:height;
    width = width > PXN8.image.width?PXN8.image.width:width;
    if (width > PXN8.aspectRatio.width && height > PXN8.aspectRatio.height && PXN8.aspectRatio.width > 0){

        if (PXN8.aspectRatio.width > PXN8.aspectRatio.height){
            height = Math.round(width/PXN8.aspectRatio.width *PXN8.aspectRatio.height);
        }else{
            width = Math.round(height/PXN8.aspectRatio.height *PXN8.aspectRatio.width);
        }
    }


    PXN8.sx = Math.round(xVals[0]/PXN8.zoom.value());
    PXN8.ex = PXN8.sx + width;
    
    PXN8.sy = Math.round(yVals[0]/PXN8.zoom.value());
    PXN8.ey = PXN8.sy + height;
    
    //
    // wph 20070105
    //
    PXN8.snapToAspectRatio();
    
    PXN8.selectArea();
    
    if (event.stopPropogation){
        event.stopPropogation(); /* DOM Level 2 */
    }else {
        event.cancelBubble = true; /*  IE */
    }
    
};

/*
 * Handler passed to beginDrag when the user is dragging on the canvas.
 * This handler will be invoked on a mouseup event
 */
PXN8.drag.upCanvasHandler = function (event)
{

    PXN8.log.append("aspect_ratio: width=" + PXN8.aspectRatio.width + ", height=" + PXN8.aspectRatio.height);

    if (!event) event = window.event ; /* IE */
    
    if (document.removeEventListener){
        document.removeEventListener("mouseup",PXN8.drag.upCanvasHandler,true);
        document.removeEventListener("mousemove",PXN8.drag.moveCanvasHandler, true);
    }else if (document.detachEvent){
        document.detachEvent("onmouseup",PXN8.drag.upCanvasHandler);
        document.detachEvent("onmousemove",PXN8.drag.moveCanvasHandler);
    }
    if (event.stopPropogation) {
        event.stopPropogation(); /*  DOM Level 2 */
    }else {
        event.cancelBubble = true; /* IE */
    }

    PXN8.listener.notify(PXN8.ON_SELECTION_COMPLETE);
};


PXN8.drag.moveSelectionBoxHandler = function (event)
{
    var _ = PXN8.dom;
    
    if (!event) event = window.event; /* IE  */
    
    var canvasBounds = _.eb("pxn8_canvas");
    var theImg = _.id("pxn8_image");
    
    var mx = canvasBounds.x + theImg.width;
    var my = canvasBounds.y + theImg.height;

    var cursorPos = _.cursorPos(event);
    var scrolledPoint = PXN8.scrolledPoint(cursorPos.x, cursorPos.y);

    /* how much (in pixels) the cursor has moved */
    var rx = scrolledPoint.x - PXN8.drag.beginDragX;
    var ry = scrolledPoint.y - PXN8.drag.beginDragY;
    
    var zrx = rx / PXN8.zoom.value();
    var zry = ry / PXN8.zoom.value();
    
    /* is it right of left border ? */
    var sx = Math.round((PXN8.drag.osx + zrx)>0?(PXN8.drag.osx + zrx):0);
    sx = Math.round((sx+PXN8.drag.ow)>PXN8.image.width?(PXN8.image.width-PXN8.drag.ow):sx);

    /*  is it below the top border ? */
    var sy = Math.round((PXN8.drag.osy + zry)>0?(PXN8.drag.osy + zry):0);
    sy = Math.round((sy+PXN8.drag.oh)>PXN8.image.height?(PXN8.image.height-PXN8.drag.oh):sy);

    var width = PXN8.drag.ow>0?PXN8.drag.ow:0;
    var height = PXN8.drag.oh>0?PXN8.drag.oh:0;
    
    //PXN8.ex = (PXN8.sx + PXN8.drag.ow)>0?(PXN8.sx+PXN8.drag.ow):0;
    //PXN8.ey = (PXN8.sy + PXN8.drag.oh)>0?(PXN8.sy+PXN8.drag.oh):0;
    
    if (event.stopPropogation) {
        event.stopPropogation(); /* DOM Level 2 */
    }else {
        event.cancelBubble = true; /* IE */
    }

    PXN8.select(sx,sy,width,height);
    
    //PXN8.selectArea();
};


/*
 * Handler passed to beginDrag when the user is dragging the selection rect around.
 * This handler will be invoked on a mouseup event
 */
PXN8.drag.upSelectionBoxHandler = function (event)
{
    if (!event) event = window.event ; /* IE */
    if (document.removeEventListener){
        document.removeEventListener("mouseup",PXN8.drag.upSelectionBoxHandler,true);
        document.removeEventListener("mousemove",PXN8.drag.moveSelectionBoxHandler, true);
    }else if (document.detachEvent){
        document.detachEvent("onmouseup",PXN8.drag.upSelectionBoxHandler);
        document.detachEvent("onmousemove",PXN8.drag.moveSelectionBoxHandler);
    }
    if (event.stopPropogation) {
        event.stopPropogation(); /* DOM Level 2 */
    }else {
        event.cancelBubble = true; /* IE */
    }
    PXN8.listener.notify(PXN8.ON_SELECTION_COMPLETE);
};
/* ============================================================================
 *
 * (c) Copyright SXOOP Technologies Ltd. 2005-2007
 * All rights reserved.
 *
 * This file contains code to draw and manage the resize handles that 
 * appear on the selection box.
 *
 */
var PXN8 = PXN8 || {};

/**************************************************************************

SECTION: Selection Area Resizing
================================
The behaviour of the selection area can be modified using a single function
which turns on or off the resize handles which appear at the sides and corners of
the selected area.

***/

/* ============================================================================
 *
 * Resizing code makes extensive use of 'currying' (functions that return 
 * functions with variables 'baked in'. If not for currying, this code would 
 * be way too long and repetitive.
 * walter higgins
 * 3 February 2006
 *
 */
PXN8.resize = {
    dx: 0,
    dy: 0,
    start_width: 0,
    start_height: 0
};

PXN8.resize.canResizeNorth = function(yOffset){
    return (PXN8.sy + yOffset < (PXN8.ey-PXN8.style.resizeHandles.size)) && (PXN8.sy + yOffset > 0);
};

PXN8.resize.canResizeWest = function(xOffset){
    return (PXN8.sx + xOffset < (PXN8.ex-PXN8.style.resizeHandles.size)) && (PXN8.sx + xOffset > 0);
};


PXN8.resize.canResizeSouth = function(yOffset){
    return (PXN8.ey + yOffset > (PXN8.sy+PXN8.style.resizeHandles.size)) && (PXN8.ey + yOffset < PXN8.image.height);
};


PXN8.resize.canResizeEast = function(xOffset){
    return (PXN8.ex + xOffset > (PXN8.sx+PXN8.style.resizeHandles.size)) && (PXN8.ex + xOffset < PXN8.image.width);
};


PXN8.resize.nTest = function(xOffset,yOffset,event){

    //if (PXN8.resize.canResizeNorth(yOffset) && PXN8.aspectRatio.width == 0)        // PXN8.sy > 0
    if (PXN8.resize.canResizeNorth(yOffset) )        // PXN8.sy > 0
    {
        PXN8.resize.dy = event.clientY;
        PXN8.sy = Math.round(PXN8.sy + yOffset);
        return true;
            
    }
    return false;
};


PXN8.resize.sTest = function(xOffset,yOffset,event){

    //if (PXN8.resize.canResizeSouth(yOffset)  && PXN8.aspectRatio.width == 0)
    if (PXN8.resize.canResizeSouth(yOffset))
    {
        PXN8.resize.dy = event.clientY;
        PXN8.ey = Math.round(PXN8.ey + yOffset);
        return true;
    }
    return false;
};


PXN8.resize.wTest = function(xOffset,yOffset,event){

    //if (PXN8.resize.canResizeWest(xOffset)  && PXN8.aspectRatio.width == 0)        // PXN8.sx > 0
    if (PXN8.resize.canResizeWest(xOffset))        // PXN8.sx > 0
    {
        PXN8.resize.dx = event.clientX;
        PXN8.sx = Math.round(PXN8.sx + xOffset);
        return true;
    }
    return false;
};


PXN8.resize.eTest = function(xOffset,yOffset,event){

    //if (PXN8.resize.canResizeEast(xOffset) && PXN8.aspectRatio.width == 0)
    if (PXN8.resize.canResizeEast(xOffset))
    {
        PXN8.resize.dx = event.clientX;
        PXN8.ex = Math.round(PXN8.ex + xOffset);
        return true;
    }
    return false;
};


PXN8.resize.nwTest = function(xOffset,yOffset,event){
    if (xOffset == 0 || yOffset == 0){
        return false;
    }
    var hr = PXN8.resize.start_height/PXN8.resize.start_width;
    var wr = 1 / hr;
    
    if (wr > hr){
        xOffset = yOffset * wr;
    }else if (wr < hr){
        yOffset = xOffset * hr;
    }else{
        yOffset = xOffset;
    }
    //
    // for NW corner
    // ensure both offsets are either negative or positive
    //
    if (xOffset > 0){
        // make Y positive if not already
        yOffset = Math.abs(yOffset);
    }else{
        // make y negative if not already
        yOffset = 0 - Math.abs(yOffset);
    }
    if (PXN8.resize.canResizeWest(xOffset) && PXN8.resize.canResizeNorth(yOffset))
    {
        PXN8.resize.dx = event.clientX;
        PXN8.resize.dy = event.clientY;
        PXN8.sx = Math.round(PXN8.sx + xOffset);
        PXN8.sy = Math.round(PXN8.sy + yOffset);
        return true;
    }
    return false;
};


PXN8.resize.swTest = function(xOffset,yOffset,event) {
    if (xOffset == 0 || yOffset == 0){
        return false;
    }
    var hr = PXN8.resize.start_height/PXN8.resize.start_width;
    var wr = 1 / hr;
    
    if (wr > hr){
        yOffset = xOffset * wr;
    }else{
        yOffset = xOffset;
    }
    
    //
    // for SW corner
    // ensure offset are +/-
    //
    if (xOffset > 0){
        // make Y negative if X is positive
        yOffset = 0 - Math.abs(yOffset);
    }else{
        // make y positive if X is negative
        yOffset = Math.abs(yOffset);
    }
    if (PXN8.resize.canResizeWest(xOffset) && PXN8.resize.canResizeSouth(yOffset))
    {
        PXN8.resize.dx = event.clientX;
        PXN8.resize.dy = event.clientY;
        PXN8.sx = Math.round(PXN8.sx + xOffset);
        PXN8.ey = Math.round(PXN8.ey + yOffset);
        return true;
    }
    return false;
};


PXN8.resize.neTest = function(xOffset,yOffset,event) {
    if (xOffset == 0 || yOffset == 0){
        return false;
    }
    var hr = PXN8.resize.start_height/PXN8.resize.start_width;
    var wr = 1 / hr;
    
    if (wr > hr){
        xOffset = yOffset * wr;
    }else{
        xOffset = yOffset;
    }
    //
    // for NE corner
    // ensure offset are +/-
    //
    if (yOffset > 0){
        // make Y negative if X is positive
        xOffset = 0 - Math.abs(xOffset);
    }else{
        // make y positive if X is negative
        xOffset = Math.abs(xOffset);
    }
    if (PXN8.resize.canResizeEast(xOffset) && PXN8.resize.canResizeNorth(yOffset))
    {  
        PXN8.resize.dx = event.clientX;
        PXN8.resize.dy = event.clientY;
        PXN8.ex = Math.round(PXN8.ex + xOffset);
        PXN8.sy = Math.round(PXN8.sy + yOffset);
        
        return true;
    }
    return false;
};


PXN8.resize.seTest = function(xOffset,yOffset,event) {
    if (xOffset == 0 || yOffset == 0){
        return false;
    }
    var hr = PXN8.resize.start_height/PXN8.resize.start_width;
    var wr = 1 / hr;
    
    if (wr > hr){
        xOffset = yOffset * wr;
    }else{
        yOffset = xOffset;
    }
    //
    // for SE corner
    // ensure offsets are both + or -
    //
    if (xOffset > 0){
        // make Y positive if X is positive
        yOffset = Math.abs(yOffset);
    }else{
        // make y negative if X is negative
        yOffset = 0 - Math.abs(yOffset);
    }
    if (PXN8.resize.canResizeEast(xOffset) && PXN8.resize.canResizeSouth(yOffset))
    {
        PXN8.resize.dx = event.clientX;
        PXN8.resize.dy = event.clientY;
        PXN8.ex = Math.round(PXN8.ex + xOffset);
        PXN8.ey = Math.round(PXN8.ey + yOffset);
        return true;
    }
    return false;
};


PXN8.resize.stopResizing = function(event){

    if (!event) event = window.event ; /* IE */
    
    if (document.removeEventListener){
        document.removeEventListener("mouseup",PXN8.resize.stopResizing,true);
        for (var i in PXN8.resize.handles){
            if (typeof PXN8.resize.handles[i] != "function"){
                document.removeEventListener("mousemove",PXN8.resize.handles[i].moveHandler, true);
            }
        }
        
    }else if (document.detachEvent){
        document.detachEvent("onmouseup",PXN8.resize.stopResizing);
        for (var i in PXN8.resize.handles){
            if (typeof PXN8.resize.handles[i] != "function"){
                document.detachEvent("onmousemove",PXN8.resize.handles[i].moveHandler);
            }
        }
    }
    if (event.stopPropogation) event.stopPropogation(); /*  DOM Level 2 */
    else event.cancelBubble = true; /* IE */

    PXN8.listener.notify(PXN8.ON_SELECTION_COMPLETE);
    
};


    /**
     * Returns a handler that get's called when the user 
     * mouses-down on one of the resize handlers
     */
PXN8.resize.startResizing = function(hdlr){
    var result = function(event){
        
        if (!event) event = window.event;
        
        PXN8.resize.dx = event.clientX;
        PXN8.resize.dy = event.clientY;
        
        var sel = PXN8.getSelection();
            
        PXN8.resize.start_height = sel.height;
        PXN8.resize.start_width = sel.width;
        
        if (document.addEventListener){
            document.addEventListener("mousemove", hdlr, true);
            document.addEventListener("mouseup", PXN8.resize.stopResizing, true);
        }else if (document.attachEvent){
            document.attachEvent("onmousemove",hdlr);
            document.attachEvent("onmouseup",PXN8.resize.stopResizing);
        }
        if (event.stopPropogation){
            event.stopPropogation();/* DOM Level 2 */
        }else {
            event.cancelBubble = true; /* IE */
        }
        
        if (event.preventDefault){
            event.preventDefault(); /* DOM Level 2 */
        }else {
            event.returnValue = false; /*  IE */
            }
        
        
    };
    return result;
};

    
PXN8.resize.createResizeHandle = function(direction,size,color) {
    var result = document.createElement("div");
    result.id = direction + "_handle";
    result.style.backgroundColor = color;
    result.style.position = "absolute";
    result.style.width = size + "px";
    result.style.height = size + "px";
    result.style.overflow = "hidden"; // fixes IE
    result.style.zIndex = 999;
    result.style.cursor = direction + "-resize";
    result.onmousedown = PXN8.resize.startResizing(PXN8.resize.handles[direction].moveHandler);
    result.ondrag = function(){return false;};
    return result;
};

    
PXN8.resize.positionResizeHandles = function() {
    var dom = PXN8.dom;

    var sel = PXN8.getSelection();
	 
    if (sel.width == 0){
        PXN8.resize.hideResizeHandles();
        return;
    }
    var zoom = PXN8.zoom.value();
    var rhsz = PXN8.style.resizeHandles.size;
    var rhsm = PXN8.style.resizeHandles.smallsize;
    
    var canvas = dom.id("pxn8_canvas");
    
    for (var i in PXN8.resize.handles){
                
        if (typeof (PXN8.resize.handles[i]) == "function"){
            continue;
        }
            
        var handle = dom.id( i + "_handle");
        if (!handle){
            handle = PXN8.resize.createResizeHandle(i, rhsz, PXN8.style.resizeHandles.color);
            dom.ac(canvas,handle);
        }
        if (handle.style.display == "none"){
            handle.style.display = "block";
        }
        PXN8.resize.handles[i].position(handle,sel);
    }
};

PXN8.resize.hideResizeHandles = function(hdls) {
    var dom = PXN8.dom;

    if (hdls){
        for (var i =0; i < hdls.length;i++){
            var handle = dom.id( i + "_handle");
            if (handle){
                handle.style.display = "none";
            }
        }
    }else{
        // hide all
        for (var i in PXN8.resize.handles){
            if (typeof (PXN8.resize.handles[i]) == "function"){
                continue;
            }
            
            var handle = dom.id( i + "_handle");
            if (handle){
                handle.style.display = "none";
            }
        }
    }
};



PXN8.resize.resizer = function( testFunc ) 
{
    var result = function(event){
        
        if (!event) event = window.event;
        var rdy = event.clientY - PXN8.resize.dy;
        var rdx = event.clientX - PXN8.resize.dx;
        /*
         * sane resizing when zoomed 
         */
        var prdy = Math.round(rdy / PXN8.zoom.value());
        var prdx = Math.round(rdx / PXN8.zoom.value());
        
        if (prdx == 0 && prdy == 0){
            // do nothing
        }else{
            //
            // testFunc will likely change the selection
            //
            if (testFunc(prdx,prdy,event) == true){
                // 
                // snap to aspect ratio
                //
                PXN8.snapToAspectRatio();
                PXN8.selectArea();
            }
        }
        
        if (event.stopPropogation) event.stopPropogation(); /* DOM Level 2 */
        else event.cancelBubble = true; /* IE */
    };
    return result;
};


/**
 * All of the resize handles are defined here
 */
PXN8.resize.handles = {
    "n":  { moveHandler: PXN8.resize.resizer(PXN8.resize.nTest),
            position: function(handle,sel){
                var sel_rect = PXN8.dom.eb("pxn8_select_rect");
                handle.style.left = sel_rect.x + Math.ceil(sel_rect.width/2) - (PXN8.style.resizeHandles.size/2) + "px";
                handle.style.top = sel_rect.y + "px";
            }
    },
    "s":  { moveHandler: PXN8.resize.resizer(PXN8.resize.sTest),
            position: function(handle,sel){
                var sel_rect = PXN8.dom.eb("pxn8_select_rect");
                handle.style.left = sel_rect.x + Math.ceil(sel_rect.width/2) - (PXN8.style.resizeHandles.size/2) + "px";
                handle.style.top = Math.round(((sel.top + sel.height) * PXN8.zoom.value()) - PXN8.style.resizeHandles.size) + "px";
            }
    },
    "e":  { moveHandler: PXN8.resize.resizer(PXN8.resize.eTest),
            position: function(handle,sel){
                var sel_rect = PXN8.dom.eb("pxn8_select_rect");
                handle.style.left = Math.round(((sel.left + sel.width) * PXN8.zoom.value()) - PXN8.style.resizeHandles.size) + "px";
                //handle.style.left = (sel_rect.x + sel_rect.width - PXN8.style.resizeHandles.size) + "px";
                handle.style.top = sel_rect.y + Math.ceil(sel_rect.height / 2) - (PXN8.style.resizeHandles.size / 2) + "px";
                //handle.style.top = Math.round((sel.top + (sel.height/2) - (PXN8.style.resizeHandles.size /2 )) * PXN8.zoom.value()) + "px";
            }
    },
    "w":  { moveHandler: PXN8.resize.resizer(PXN8.resize.wTest),
            position: function(handle,sel){
                var sel_rect = PXN8.dom.eb("pxn8_select_rect");
                //handle.style.left = Math.round(sel.left * PXN8.zoom.value()) + "px";
                //handle.style.top = Math.round((sel.top + (sel.height/2) - (PXN8.style.resizeHandles.size /2 )) * PXN8.zoom.value()) + "px";
                handle.style.top = sel_rect.y + Math.ceil(sel_rect.height / 2) - (PXN8.style.resizeHandles.size / 2) + "px";
                handle.style.left = sel_rect.x + "px";
            }
    },
    "nw": { moveHandler: PXN8.resize.resizer(PXN8.resize.nwTest),
            position: function(handle,sel){
                handle.style.left = Math.round(sel.left * PXN8.zoom.value()) + "px";
                handle.style.top = Math.round((sel.top * PXN8.zoom.value())) + "px";
            }
    },
    "sw": { moveHandler: PXN8.resize.resizer(PXN8.resize.swTest),
            position: function(handle,sel){
                handle.style.left = Math.round(sel.left * PXN8.zoom.value()) + "px";
                handle.style.top = Math.round(((sel.top + sel.height) * PXN8.zoom.value()) - PXN8.style.resizeHandles.size) + "px";
            }
    },
    "ne": { moveHandler: PXN8.resize.resizer(PXN8.resize.neTest),
            position: function(handle,sel){
                handle.style.left = Math.round(((sel.left + sel.width) * PXN8.zoom.value()) - PXN8.style.resizeHandles.size) + "px";
                handle.style.top = Math.round((sel.top * PXN8.zoom.value())) + "px";
            }
    },
    "se": { moveHandler: PXN8.resize.resizer(PXN8.resize.seTest),
            position: function(handle,sel){
                handle.style.left = Math.round(((sel.left + sel.width) * PXN8.zoom.value()) - PXN8.style.resizeHandles.size) + "px";
                handle.style.top = Math.round(((sel.top + sel.height) * PXN8.zoom.value()) - PXN8.style.resizeHandles.size) + "px";
            }
    }
};

/**************************************************************************
PXN8.resize.enable()
====================
Enable or disable the resize handles which appear at the corners and sides of the selected
area. 

Parameters
----------
* handles : An array of handles to enable or disable.
* enabled : A boolean specifying whether to enable or disable the supplied handles.

Examples
--------
To disable (hide) all of the handles...

    PXN8.resize.enable(["n","s","e","w","nw","ne","sw","se"],false);

To enable the side handles...

    PXN8.resize.enable(["n","s","e","w"],true);

<img src="pigeon300x225resizehandles.jpg"/>

***/
PXN8.resize.enable = function(handles, enable)
{
    var source = PXN8.resize.handles;
    var target = PXN8.resize.handle_store;
    var display = "none";
    var dom = PXN8.dom;
    

    if (enable){
        source = PXN8.resize.handle_store;
        target = PXN8.resize.handles;
        dispay = "block";
    }

    for (var i = 0; i < handles.length; i++){
        var handle = handles[i];
        
        var hdl = source[handle];
        delete source[handle];
        
        
        if (hdl){
            target[handle] = hdl;
            var handle_element = dom.id( handle + "_handle");
            
            if (handle_element){
                handle_element.parentNode.removeChild(handle_element);
                
                //handle_element.style.dispay = display;
            }
        }
    }

    PXN8.resize.positionResizeHandles();
    
};

PXN8.resize.handle_store = {};

/** ============================================================================
 *  
 */
PXN8.listener.add(PXN8.ON_SELECTION_CHANGE, PXN8.resize.positionResizeHandles);
/* ============================================================================
 *
 * (c) Copyright SXOOP Technologies Ltd. 2005-2007
 * All rights reserved.
 *
 * This file contains code for manipulating the DOM (document object model)
 */
var PXN8 = PXN8 || {};
/**************************************************************************

SECTION: DOM manipulation utility Functions
===========================================
Pixenate uses a number of the following DOM manipulation functions which can
be used independently of Pixenate.
***/

PXN8.dom = {
    /**
     * Computing the style is expensive.
     * cache computation results here.
     */
    cachedComputedStyles: {}
};

/***************************************************************************

PXN8.dom.cl()
=============
Remove all child nodes from the specified element. This method repeated calls
Element.removeChild(Element.firstChild) until there are no children left.

Parameters
----------
* element : A HTML Element or the element's id attribute value.

Returns
-------
element. The element which was passed in.

Examples
--------
    var el = PXN8.dom.cl(document.getElementById("myElement"));
    // this is equivalent to...
    var el = PXN8.dom.cl("myElement");
    
Related
-------
PXN8.dom.ac PXN8.dom.ce PXN8.dom.id

***/
PXN8.dom.cl = function(elt)
{
    if (typeof elt == 'string'){ elt = PXN8.dom.id(elt); }
    if (!elt) return false;
    while (elt.firstChild){ elt.removeChild(elt.firstChild);}
    return elt;
};

/***************************************************************************

PXN8.dom.tx()
=============
A convenience function which is shorthand for document.createTextNode().

Parameters
----------
* string : The text from which to create a new TextNode.

Returns
-------
The newly created TextNode object.

Example
-------
     var tn = PXN8.dom.tx("Hello World"); 
     document.body.appendChild(tn);

Related
-------
PXN8.dom.ac PXN8.dom.ce

***/
PXN8.dom.tx = function(str){ return document.createTextNode(str);};

/***************************************************************************

PXN8.dom.id()
=============
A Convenience function which is shorthand for document.getElementById()

Parameters
----------
* string : The id attribute of the element to return.

Returns 
-------

The DOM Element object which has the matching id attribute. If no match is found
then *null* is returned instead.

Example
-------
    example: var el = PXN8.dom.id("myElementId");

***/
PXN8.dom.id = function(str){ return document.getElementById(str);};

/**************************************************************************

PXN8.dom.ce()
=============

Description 
-----------
A convenience function which is shorthand for document.createElement(). This function also
allows you to set attributes for the element.

Parameters 
----------
* nodeType  
The nodename of the type of element you want to create, for example,
if you want to create an *img* element then the nodename will be 'img'.

* attributes (optional)  
An object whose properties are the attribute/value pairs for the new element.

Returns
-------
The newly created DOM Element object.

Example
-------
     // create an image with src attribute = "myimage.jpg" and a 4 pixel border.
     var img = PXN8.dom.ce("img", { src: "myimage.jpg" border: 4});

Related
-------
PXN8.dom.ac

***/
PXN8.dom.ce = function(nodeType,attrs)
{
    var el = document.createElement(nodeType);
    for (var i in attrs){ el[i] = attrs[i];}
    return el;
};

/***************************************************************************

PXN8.dom.ac()
=============
A convenience function which is shorthand for Element.appendChild().

Parameters
----------
* parent  
The parent element to which the child element will be appended.
* child  
The child element to append.

Returns
-------
The newly appended *child* DOM Element object.

Example
-------
    var address = document.getElementById("address");
    var user = document.getElementById("user");
    PXN8.dom.ac(user,address);
    // the 'address' is now a child element of the 'user' element

Related
-------
PXN8.dom.ce

***/
PXN8.dom.ac = function(parent,child)
{ 
    if (typeof parent == 'string'){ parent = PXN8.dom.id(parent); }
    if (typeof child == 'string'){ child = PXN8.dom.id(child); }
    if (!parent || !child){ return false; }
    
    parent.appendChild(child);
    return child;
};

/***************************************************************************

PXN8.dom.eb()
=============
Get the Bounds (size and coordinates) of an element on the page.

Parameters
----------
* element : An Element Object or the element's id attribute.

Returns
-------
The element bounds for an element. Bounds are in the form of an object with the following
properties... *x*, *y*, *width*, *height*

Example
-------
     var bounds = PXN8.dom.eb("myElementId");
     // bounds = {x: 48, y: 105, width: 200, height: 120}

Related
-------
PXN8.dom.ep PXN8.dom.cursorPos PXN8.dom.windowSize

***/
PXN8.dom.eb = function(elt)
{
    if (typeof elt == 'string'){ elt = PXN8.dom.id(elt); }
    if (!elt){ return false; }
                
    var x = null;
    var y = null;
    
    if(elt.style.position == "absolute") 
    {
        x = parseInt(elt.style.left);
        y = parseInt(elt.style.top);
    } else {
        var pos = this.ep(elt); 
        x = pos.x;
        y = pos.y;
    } 
    return {x: x, y: y, width: elt.offsetWidth, height: elt.offsetHeight};
};
/***************************************************************************

PXN8.dom.ep()
=============
Given an element, calculate it's absolute position relative to the BODY element.

Parameters
----------
* element : An Element object or it's id attribute.

Returns 
-------
An object with attributes x and y representing the 
coordinates of the top left corner of the element

Example
-------
     var pos = PXN8.dom.ep("myElementId"); 
     // pos = { x: 48, y: 105 }
Related
-------
PXN8.dom.eb PXN8.dom.cursorPos

***/
PXN8.dom.ep = function (elt)
{
    if (typeof elt == 'string'){ elt = PXN8.dom.id(elt); }
    if (!elt) { return false; }
    
    var tmpElt = elt;
    var posX = parseInt(tmpElt["offsetLeft"]);
    var posY = parseInt(tmpElt["offsetTop"]);
    while(tmpElt.tagName.toUpperCase() != "BODY") {
        tmpElt = tmpElt.offsetParent;
        // IE7 bug ? - in one case the body tag was overlooked.
        if (tmpElt == null){
            break;
        }
        posX += parseInt(tmpElt["offsetLeft"]);
        posY += parseInt(tmpElt["offsetTop"]);
    } 
    return {x: posX, y:posY};
};


/***************************************************************************

PXN8.dom.windowSize()
=====================
Calculate the size of the browser window (it's outer width & height).

Returns
-------
An object with 'height' and 'width' properties.

Example
-------
     var winSz = PXN8.dom.windowSize();
     // winSz = { height: 768, width: 1002}

Related
-------
PXN8.dom.eb PXN8.dom.cursorPos PXN8.dom.ep

***/
PXN8.dom.windowSize = function() 
{
    if (document.all){
        return {width: document.body.clientWidth, 
                height: document.body.clientHeight};
    }else{
        return {width: window.outerWidth,
                height: window.outerHeight};
    } 
};
/***************************************************************************

PXN8.dom.opacity()
==================
Sets the opacity of a given element.

Parameters
----------
* element : The elment object or it's id attribute.
* value : The opacity expressed as a number between 0 and 1.0.

Example
-------
    PXN8.dom.opacity("myElementId",0.5);
    // sets opacity of the 'myElementId' element to 50%

***/
PXN8.dom.opacity = function(elt, value)
{
    if (typeof elt == 'string'){ elt = PXN8.dom.id(elt); }
    /*
     * it's quite possible that the element has been deleted
     */
    if (!elt){
        return;
    }
    if (document.all){
        elt.style.filter = "alpha(opacity:" + (value*100) + ")";
    }else{
        elt.style.opacity = value;
        elt.style._moz_opacity = value;
    }
};

/***************************************************************************

PXN8.dom.clz()
==============
Return an array of elements with the supplied classname

Parameters
----------
* className : A string containing the name of the class.

Returns
-------
An array of Element objects with a matching *class* attribute.

Example
-------
    <span class="finance">Loan: $40,000</span>
    <span class="address">8092 Wolverdale drive</span>
    <span class="finance">Repayments: $800</span>
    <script type="text/javascript">
     var allFinanceElements = PXN8.dom.clz("finance");
     // allFinanceElements = [SpanElement{8092 Wolverdale drive}, SpanElement{Repayments: $800}]
    </script>

Related
-------
PXN8.dom.addClass PXN8.dom.removeClass

***/
PXN8.dom.clz = function(className)
{
    var links = document.getElementsByTagName("*");
    
    var result = new Array();
    for (var i = 0;i < links.length; i++){
        if (links[i].className.match(className)){
            result.push(links[i]);
        }
    }
    return result;
};

/***************************************************************************

PXN8.dom.removeClass()
======================
Removes a class from the element's className (or 'class' attribute).

Parameters
----------
* element : An element or it's attribute id, from which you want to remove the class.

* className : The name of the class to remove.

Example
-------

     // before 
     // <span id="myElementId" class="finance private">Repayments: $800</span>
     PXN8.dom.removeClass("myElementId","private");
     // after
     // <span id="myElementId" class="finance">Repayments: $800</span>
Related
-------
PXN8.dom.addClass PXN8.dom.clz

***/
PXN8.dom.removeClass = function(elt,className)
{
    if (typeof elt == 'string'){ elt = PXN8.dom.id(elt); }
    var oldClassname = elt.className;
    var oldClasses = oldClassname.split(/ /);
    var newClasses = [];
    for (var i = 0;i < oldClasses.length; i++){
        if (oldClasses[i] != className){
            newClasses.push(oldClasses[i]);
        }
    }
    var newClassname = newClasses.join(' ');
    elt.className = newClassname;
};

/***************************************************************************

PXN8.dom.addClass()
===================
Add a new class to the element's className (or *class* attribute).

Parameters
----------

* element : The element or it's id attribute.
* className : The name of the class to add to this element.

Example 
-------
Before...
    <div class="panel">...</div>
Javascript...
    PXN8.dom.addClass("myElementId","dragable");
After...
    <div class="panel dragable">...</div>

Related
-------
PXN8.dom.removeClass PXN8.dom.clz

***/
PXN8.dom.addClass = function(elt,className)
{
    if (typeof elt == 'string'){ elt = PXN8.dom.id(elt); }
    elt.className += ' ' + className;
};

/***************************************************************************

PXN8.dom.isClass()
==================
Does the element's className contain the supplied classname ?

Parameters
----------

* element : The element object or it's id attribute.
* className : The name of the class for which to check.

Returns
-------
*true* or *false* .

Related
-------
PXN8.dom.addClass PXN8.dom.removeClass

***/
PXN8.dom.isClass = function(elt,className)
{
    if (typeof elt == 'string'){ elt = PXN8.dom.id(elt); }
    var clzs = elt.className.split(/ /);
    for (var i = 0; i < clzs.length; i++){
        if (clzs[i] == className){
            return true;
        }
    }
    return false;
};

/***************************************************************************

PXN8.dom.computedStyle()
========================
Returns the style of an element based on it's external stylesheet, and any inline styles.

Parameters
----------
* elementId : The id of the element whose style must be computed

Returns
-------
A CSSStyleDeclaration object.

***/
PXN8.dom.computedStyle = function(elementId)
{
    var result = null;
    
    if (this.cachedComputedStyles[elementId]){
        result = this.cachedComputedStyles[elementId];
    }else{
        var element = this.id(elementId);
        if (document.all){
            
            result = element.currentStyle;
            
        }else{
            if (window.getComputedStyle){
                result = window.getComputedStyle(element,null);                
            }else{
                /**
                 * Safari doesn't support getComputedStyle() 
                 */
                result = element.style;
            }
        }
        this.cachedComputedStyles[elementId] = result;
    }
    return result;
};
/***************************************************************************

PXN8.dom.cursorPos()
====================
Return the current adjusted cursor position.

Parameters
-----------

* event : The event (a MouseEvent) from which to obtain the cursor position.

Returns
-------
An object with *height* and *width* parameters.

Related
-------
PXN8.dom.ep PXN8.dom.eb

***/
PXN8.dom.cursorPos = function (e) 
{
    e = e || window.event;
    var cursor = {x:0, y:0};
    if (e.pageX || e.pageY) {
        cursor.x = e.pageX;
        cursor.y = e.pageY;
    }
    else {
        var sl = document.documentElement.scrollLeft || document.body.scrollLeft;
        var st = document.documentElement.scrollTop || document.body.scrollTop;
        cursor.x = e.clientX + sl - document.documentElement.clientLeft;
        cursor.y = e.clientY + st - document.documentElement.clientTop;
    }
    return cursor;
};

/***************************************************************************

PXN8.dom.addLoadEvent()
=======================
Add a callback to the window's onload event. This is a convenience function which enables 
multiple functions to be called when the *window.onload* event is fired.

Parameters
----------
* function : The callback function to be called when the window has loaded.

Example
-------
    PXN8.dom.addLoadEvent(function(){
        alert("The window has loaded");
    });

***/
PXN8.dom.addLoadEvent = function(func)
{
    var oldonload = window.onload;
    if (typeof window.onload != 'function'){
        window.onload = func;
    } else {
        window.onload = function(){
            if (oldonload){
                oldonload();
            }
            func();
        };
    }
};

PXN8.dom.addClickEvent = function(elt,func)
{
    var oldonclick = elt.onclick;
    if (typeof oldonclick != 'function'){
        elt.onclick = func;
    } else {
        elt.onclick = function(){
            if (oldonclick){
                oldonclick(elt);
            }
            func(elt);
        };
    }
};
PXN8.dom.onceOnlyClickEvent = function(elt,func)
{
    var oldonclick = elt.onclick;
    
    PXN8.dom.addClickEvent(elt,function(){
        func();
        elt.onclick = oldonclick;
    });
};
/**
 * Make constructing tables easier
 *  param rows An array of arrays (2-dimensional) 
 *    Each item in the inner arrays must be either a string or a DOM element.
 *    
 *  e.g. PXN8.dom.table([[ "Row1Col1", document.getElementById("pxn8_image") ],
 *                         [ "Row2Col1", "Hello World" ],
 *                         [ "This spans two columns"  ]]);
 */
PXN8.dom.table = function(rows, attributes)
{
    var dom = PXN8.dom;
    
    var result = dom.ce("table",attributes);
    var tbody = dom.ce("tbody");
	 result.appendChild(tbody);
    /**
     * First scan the array to find find the widest row (most cells)
     */
    var mostCells = 0;
    for (var i = 0; i < rows.length; i++){
        var row = rows[i];
        if (row.length > mostCells){
           mostCells = row.length;
        }
    }
   
    for (var i = 0; i < rows.length; i++){
        var tr = dom.ce("tr");
	     tbody.appendChild(tr);
        var rowData = rows[i];
        var cellsInRow = rowData.length;
        for (var j = 0; j < rows[i].length; j++){
            var cellData = rows[i][j];

            var td = dom.ce("td");
	         tr.appendChild(td);
            if (j == rows[i].length -1 && cellsInRow < mostCells){
                td.colSpan = (mostCells - cellsInRow)+1;
            }
            if (typeof cellData == "string"){
	             td.appendChild(dom.tx(cellData));
            }else if (PXN8.isArray(cellData)){
                // it's an array
                for (var k = 0; k < cellData.length; k++){
                    if (typeof cellData[k] == "string"){
	                     td.appendChild(dom.tx(cellData[k]));
                    }else{
	                     td.appendChild(cellData[k]);
                    }
                }
            }else{
	             td.appendChild(cellData);
            }
        }
    }	
    return result;
};

/**
 * Add a Flag icon to the page, placing it at x,y with ID of id
 * Returns the <IMG> flag element
 */
PXN8.dom.createFlag = function(x,y,id)
{
    var flag = PXN8.dom.ce("img",{"src": PXN8.root + "images/icons/flag.gif", "id": id});
    document.body.appendChild(flag);
    flag.style.position = "absolute";
    flag.style.top = y - 16 +  "px";
    flag.style.left = x - 11 + "px";
    return flag;
};

/**
 * (c) 2006-2007 Sxoop Technologies Ltd.
 * 
 * This javascript file defines all of the image operations used by 
 * pxn8_tools_ui.js
 *
 */

var PXN8 = PXN8 || {};

/*****************************************************************************

SECTION: Photo-Editing functions
================================
As well as the core selection, zoom and initialization functions, the Pixenate&trade; 
javascript API is composed of a series of photo-editing functions which perform various
modifications to the photo by calling equivalent CGI functions in the Pixenate software running on 
the web server.

PXN8.tools
==========
This variable defines a namespace within which all of the Pixenate photo-editing 
functions are defined.
***/

PXN8.tools = {};

/*****************************************************************************

PXN8.tools.history()
====================
Go back in time 'offset' number of operations. Clients should not call this function
directly. Use the *PXN8.tools.undo()* and *PXN8.tools.redo()* functions instead.

Parameters
----------
* offset : A number (positive or negative) indicating how many operations to move forwards or backwards
in the user session stack.

Examples
--------
    PXN8.tools.history(-1);
    // same as PXN8.tools.undo();

    PXN8.tools.history(+1)
    // same sas PXN8.tools.redo();

Related
-------
PXN8.tools.undo PXN8.tools.redo

***/
PXN8.tools.history = function (offset)
{
    if (offset == 0){
        return;
    }
    if (PXN8.isUpdating()){
        alert (PXN8.strings.IMAGE_UPDATING);
        return;
    }
    var theImage = PXN8.dom.id("pxn8_image");
    if (!theImage){
        alert("Please wait for the image to load");
        return;
    }
    
    
    if (!offset) offset = -1;
    if (PXN8.opNumber == 0 && offset < 0){
        PXN8.show.alert(PXN8.strings.NO_MORE_UNDO);
        return;
    }
    if (PXN8.opNumber == PXN8.maxOpNumber && offset > 0){
        PXN8.show.alert(PXN8.strings.NO_MORE_REDO);
        return;
    } 
    
    if (offset < 0){

        var userOp = PXN8.getUserOperation(PXN8.opNumber);
        
        PXN8.show.alert("- " + userOp.operation, 500);
    }else{
        PXN8.log.append("redo: " + PXN8.opNumber);

        for (var i = 0;i < PXN8.history.length; i++){
            PXN8.log.append("redo: " + PXN8.history[i].operation);
        }
        var userOp = PXN8.getUserOperation(PXN8.opNumber+1);
        
        PXN8.show.alert("+ " + userOp.operation,500);
    }
    
    var index =  PXN8.opNumber + offset;

    var currentImageData = PXN8.images[index];

    if (!currentImageData){
        // 
        // wph 20070223: 
        // this could have potentially happenend
        // if the user clicked 'undo' before the new image loaded
        //
        alert("Error! PXN8.images[" + index + "] is undefined");
        return false;
    }
    
    PXN8.opNumber = index;
    
    PXN8.image.location = currentImageData.location;
    PXN8.image.width = currentImageData.width;
    PXN8.image.height = currentImageData.height;
    
    // point image at the array element was bad !
    // changes to PXN8.image were also reflected in 
    // the array element leading to a long bug-tracking session
    // REMEMBER this !!!
    //PXN8.image = PXN8.images[PXN8.opNumber];

    /**
     * wph 20070108 : don't unselect because some ON_IMAGE_CHANGE listeners
     * might want to automatically select the image whenever it's changed.
     * Better to unselect _before_ notifying listeners
     */
    PXN8.unselect();
    PXN8.replaceImage(PXN8.image.location);
    return false;
};

/**************************************************************************

PXN8.tools.undo
===============
Undo the last operation.

Related
-------
PXN8.tools.undoall PXN8.tools.redo PXN8.tools.redoall PXN8.tools.history

***/
PXN8.tools.undo = function()
{
    PXN8.tools.history(-1);
    return false;
};

/***************************************************************************

PXN8.tools.redo()
=================
Redo the last undone operation.

Related
-------
PXN8.tools.undo PXN8.tools.redoall PXN8.tools.history

***/
PXN8.tools.redo = function()
{
    PXN8.tools.history(+1);
    return false;
};
/***************************************************************************

PXN8.tools.undoall()
====================
Undo all changes that were made to the photo

Related
-------
PXN8.tools.undo PXN8.tools.redoall PXN8.tools.history PXN8.tools.redo

***/
PXN8.tools.undoall = function()
{
    PXN8.tools.history(0 - PXN8.opNumber);
    return false;
};

/**************************************************************************

PXN8.tools.redoall()
====================
Redo all changes made to the photo.

Related
-------
PXN8.tools.undo PXN8.tools.redoall PXN8.tools.history PXN8.tools.redo
***/
PXN8.tools.redoall = function()
{
    PXN8.tools.history(PXN8.maxOpNumber-PXN8.opNumber);
    return false;
};

/**************************************************************************

PXN8.tools.updateImage()
========================
This function takes an array of operations as a parameter and submits the 
operations to the server to update the image. This function is called by all other PXN8.tools.*
functions (except PXN8.tools.undo(), PXN8.tools.redo(),  PXN8.tools.undoall(), PXN8.tools.redoall() ).


Parameters
----------
* operations : An array of 'operations' which will be performed on the image.

Example
-------
The following code will crop the photo and rotate it by 90&deg; ...

    PXN8.tools.updateImage([
                            {operation: "crop", top: 40, left: 40, width: 200, height: 200},
                            {operation: "rotate", angle: 90}
                           ]);
                            
PXN8.tools.updateImage() can be used to combine multiple image-editing operations into a single
user operation (to the user it appears to be one operation even though the image goes through 
two transformations, being first normalized and then enhanced).
When the user clicks *Undo*, both the *crop* and *rotate* operations will be undone. 
PXN8.tools.updateImage() is really useful if you would like to create your own custom 'quick-fix' 
operations which are combinations of existing operations. 
Please see <a href="#api_operations">API Operations</a> for a full list of operations

***/
PXN8.tools.updateImage = function(ops)
{
    var theImage = PXN8.dom.id("pxn8_image");
    if (!PXN8.ready){
        alert("Please wait for the image to load");
        return;
    }

    /**
     * wph 20060909 : Don't increment PXN8.opNumber unless the
     * last operation has completed.
     */
    if (PXN8.isUpdating()){
        alert (PXN8.strings.IMAGE_UPDATING);
        return;
    }
    /**
     * increment PXN8.opNumber & add op to the history
     */
    PXN8.addOperations(ops);
        
};

/**************************************************************************

PXN8.tools.enhance()
====================
Apply a digital filter to enhance a noisy photo. This is useful for smoothing facial lines.

***/
PXN8.tools.enhance = function()
{
    PXN8.tools.updateImage([{operation: "enhance"}]);
};

/**************************************************************************

PXN8.tools.normalize()
======================
Transform photo to span the full range of color values. This results in a more
colorful, better balanced image.

***/
PXN8.tools.normalize = function()
{
    PXN8.tools.updateImage([{operation: "normalize"}]);
};

/**************************************************************************

PXN8.tools.instantFix()
=======================
instant_fix performs both 'enhance' and 'normalize'

Related
-------
PXN8.tools.enhance PXN8.tools.normalize

***/
PXN8.tools.instantFix = function()
{
    PXN8.tools.updateImage([ {operation: "normalize"},
                             {operation: "enhance"}
                           ]);
};

/**************************************************************************

PXN8.tools.spiritLevel()
========================
Fix the horizon on a photo: Uses two points (left and right) to ascertain 
what the correct angle of the photo should be.
This function is a wrapper for PXN8.tools.rotate().

Parameters
----------
* x1 : the X coordinate of the first point
* y1 : The Y coordinate of the first point
* x2 : The X coordinate of the second point
* y2 : The Y coordinate of the second point

Related 
-------
PXN8.tools.rotate

***/
PXN8.tools.spiritlevel = function(x1,y1,x2,y2)
{

    var opposite = y1 > y2 ? y1 - y2 : y2 - y1;
    var adjacent = x1 > x2 ? x1 - x2 : x2 - x1;
    var hypotenuse = Math.sqrt((opposite * opposite) + (adjacent * adjacent));
    var sineratio = opposite / hypotenuse;
    var RAD2DEG = 57.2957795;
    var rads = Math.atan2(sineratio,Math.sqrt(1 - (sineratio * sineratio)));
    var degrees = rads * RAD2DEG;
    if (y1 < y2){
        degrees = 360 - degrees;
    }
    PXN8.tools.rotate ({angle: degrees});
        
};
/**
 * -- TODO: document PXN8.tools.spiritlevel_mode for API reference
 * wph 20070426 : A new mode which simplifies the UI interaction for using the spirit-level tool.
 */

PXN8.tools.spiritlevel_mode = {};
PXN8.tools.spiritlevel_mode.clicks = [];
PXN8.tools.spiritlevel_mode.callback = null;
PXN8.tools.spiritlevel_mode.start = function(callback){
    var _ = PXN8.dom;
    var img = _.id("pxn8_image");
    var iw = img.width / PXN8.zoom.value();
    var ih = img.height / PXN8.zoom.value();
    var sel = _.id("pxn8_select_rect");
    
    sel.style.cursor = "pointer";
    PXN8.crosshairs.setEnabled(false);
    PXN8.resize.enable(["n","s","e","w","ne","se","nw","sw"],false);
    
    PXN8.select({top:0,left:0,width: iw/2,height: ih});
    PXN8.event.addListener(sel,"click",this.onclick);
    
    this.callback = callback;
};

PXN8.tools.spiritlevel_mode.end = function(){
    var _ = PXN8.dom;
    
    var pin1 = _.id('pxn8_flag_0');
    if (pin1){
        document.body.removeChild(pin1);
    }
    var pin2 = _.id('pxn8_flag_1');
    if (pin2){
        document.body.removeChild(pin2);
    }
    PXN8.unselect();
    var sel = _.id("pxn8_select_rect");
    sel.style.cursor = "move";
    PXN8.crosshairs.setEnabled(true);
    
    PXN8.resize.enable(["n","s","e","w","ne","se","nw","sw"],true);
    
    PXN8.event.removeListener(sel,"click",this.onclick);
    this.clicks = [];
};


PXN8.tools.spiritlevel_mode.onclick = function(event){
    var _ = PXN8.dom;
    var self = PXN8.tools.spiritlevel_mode;
    
    var img = _.id("pxn8_image");
    var iw = img.width / PXN8.zoom.value();
    var ih = img.height / PXN8.zoom.value();
    var sel = _.id("pxn8_select_rect");
    var pos = _.cursorPos(event);
    
    var flag = PXN8.dom.createFlag(pos.x,pos.y,"pxn8_flag_" + self.clicks.length);
    
    self.clicks.push(pos);
    
    PXN8.select({top:0,left:iw/2,width: iw/2,height: ih});
    
    if (self.clicks.length == 2){
        PXN8.event.removeListener(sel,"click",self.onclick);
        PXN8.tools.spiritlevel(self.clicks[0].x,self.clicks[0].y,self.clicks[1].x,self.clicks[1].y);
        
        var oldSize = PXN8.dom.eb("pxn8_image");
        
        PXN8.listener.onceOnly(PXN8.ON_IMAGE_CHANGE,function(){
                self.end();
                var newSize = PXN8.dom.id("pxn8_image");
                var hdiff = newSize.height - oldSize.height;
                var wdiff = newSize.width - oldSize.width;
                PXN8.select(wdiff,hdiff,oldSize.width-wdiff,oldSize.height-hdiff);
                
                self.callback();
            });
        
    }
};

/**************************************************************************

PXN8.tools.rotate()
===================
Rotate a photo or flip it.

Parameters
----------
* params : An object which must have at least one of the following properties...
  * angle : A number specifing the degrees through which the photo should be rotated.
  * flipvt : A boolean indicating whether or not the photo should be flipped vertically.
  * fliphz : A boolean indicating whether or not the photo should be flipped horizontally.

Examples
--------
To rotate a photo 90 degrees clockwise...
   
    PXN8.tools.rotate({angle: 90});

To flip a photo along the horizontal pane (mirror photo)...

    PXN8.tools.rotate({fliphz: true});

***/
PXN8.tools.rotate = function(params)
{
    if (!params.angle){ 
        params.angle = 0;
    }
    if (params.fliphz == null){
        params.fliphz = false;
    }
    if (params.flipvt == null){
        params.flipvt = false;
    }
    params.operation = "rotate";

    if (params.angle > 0 || params.flipvt || params.fliphz){
        PXN8.tools.updateImage([params]);
    }
};

/**************************************************************************

PXN8.tools.blur()
=================
Blur an area of the photo (or the entire photo).

Examples
--------
To blur the entire photo with a radius of 2x2...

    PXN8.tools.blur({radius: 2});

To blur an area of the photo...

    PXN8.tools.blur({radius: 2, top: 4, left: 40, width: 400, height: 200});

***/
PXN8.tools.blur = function (params)
{
    params.operation = "blur";
    PXN8.tools.updateImage([params]);
};

/**************************************************************************

PXN8.tools.colors()
===================
Change the brightness, saturation ,hue and contrast of a photo.

Examples
--------
To increase saturation by 20%...

    PXN8.tools.colors({saturation: 120});

To increase contrast & reduce brightness by 20 %

    PXN8.tools.colors({contrast: 1, brightness: 80});

To increase saturation, brightness, hue and contrast...

    PXN8.tools.colors ({brightness: 110, saturation: 110, hue: 180, contrast: 2});

Contrast must be in the range -5 to +5.
All other parameters must be in the range 0 - 200

***/
PXN8.tools.colors = function(param)
{
    if (!param.saturation) param.saturation = 100;
    if (!param.brightness) param.brightness = 100;
    if (!param.hue) param.hue = 100;
    if (!param.contrast) param.contrast = 0;
    param.operation = "colors";
    PXN8.tools.updateImage([param]);
};

/**************************************************************************

PXN8.tools.crop()
=================
Crop a photo to the dimensions provided. The most common way to call this is 
as follows...
    
    PXN8.tools.crop();

... which will simply crop the photo to the currently selected area. This is 
equivalent to ...
    PXN8.tools.crop(PXN8.getSelection());

Parameters
----------
* geometry : An object with *width*, *height*, *top* and *left* properties.

Examples
--------
Crop the photo begining at 10 pixels from left, 200 pixels from the top and 
extending 40 pixels to the right and 80 pixels to the bottom...

    PXN8.tools.crop({top: 10, left: 200, width: 40, height: 80});

***/
PXN8.tools.crop = function (params) 
{
    /**
     * wph 20070526 - use current selection if no params provided
     */
    if (!params){
        params = PXN8.getSelection();
    }
    
    if (params.width <= 0 || params.height <= 0){
        PXN8.show.alert(PXN8.strings.CROP_SELECT_AREA);
        return;
    }
    params.operation = "crop";
    PXN8.tools.updateImage([params]);
};

/**************************************************************************

PXN8.tools.preview_crop
=======================
A utility function to allow the user to preview what a crop based on the current 
selection would look like.

Parameters
----------
* timeoutMillis : Exit the preview mode after the timeoutMillis milliseconds.

***/
PXN8.tools.preview_crop = function (timeout)
{
    timeout = timeout || 3500;
    
    var preview = function(borderColor,borderOpacity,handleOpacity){
        var _ = PXN8.dom;
        var rects = ["left","right","top","bottom"];
        for (var i  = 0;i < rects.length; i++){
            var rect = _.id("pxn8_" + rects[i] + "_rect");
            rect.style.backgroundColor = borderColor;
            _.opacity(rect,borderOpacity);
        }
        for (var i in PXN8.resize.handles){
            if (typeof PXN8.resize.handles[i] != "function"){
                var handle = _.id( i + "_handle");
                _.opacity(handle,handleOpacity);
            }
        }
    };
    preview("white",1.00,0);
    
    setTimeout(function(){
            var _ = PXN8.style.notSelected;
            preview(_.color,_.opacity,1.00);
        },timeout);
};

/**************************************************************************

PXN8.tools.filter()
===================
Apply a 'lens-filter' effect to the photo.This mimics the effect
of using those tinted lens filters on SLR to create more dramatic
skies.

Parameters
----------
* params : An object with *top*, *color* and *opacity* properties. (see below).

Examples
--------
The following javascript code will produce the image on the right...

    PXN8.tools.filter({top: 125, color: '#ffa500', opacity: 80});

<table>
<tr><td>Before</td><td>After</td></tr>
<tr><td><img src="pigeon300x225.jpg"/></td><td><img src="pigeon300x225filter.jpg"/></td></tr>
</table>

The *top* property specifies where the filter should trail off completely. The filter
is always applied starting at the top of the photo. If you would like the filter to begin
at the bottom of the photo you should use the following code instead...

    PXN8.tools.updateImage([
                           {operation: "rotate", flipvt: true},
                           {operation: "filter", top: 125, color: '#ffa500', opacity: 80},
                           {operation: "rotate", flipvt: true},
                           ]);

<table>
<tr><td>Before</td><td>After</td></tr>
<tr><td><img src="pigeon300x225.jpg"/></td><td><img src="pigeon300x225filterflip.jpg"/></td></tr>
</table>

***/
PXN8.tools.filter = function (params)
{
    params.color = escape(params.color);
    params.operation = "filter";
    PXN8.tools.updateImage([params]);
};

/**************************************************************************

PXN8.tools.interlace()
======================
Adds TV-like scan-lines overlaying the photo to make it appear like it is a 
screen-grab from broadcast TV.

Parameters
----------
* params : An object with *color* and *opacity* properties.

Examples
--------

    PXN8.tools.interlace({color: '#ffffff', opacity: 66 });

<table>
<tr><td>Before</td><td>After</td></tr>
<tr><td><img src="pigeon300x225.jpg"/></td><td><img src="pigeon300x225interlace.jpg"/></td></tr>
</table>

***/
PXN8.tools.interlace = function(params)
{
    params.color = escape(params.color);
    params.operation = "interlace";
    PXN8.tools.updateImage([params]);
};

/**************************************************************************

PXN8.tools.lomo()
=================
This function adds a 'lomo' effect to the photo. This is an atmospheric and artistic 
effect that darkens the corners and (optionally) saturates the colors so that the photo
appears to have been taken using a Russian LOMO&trade; camera.

Parameters
----------
* params : An object with *opacity* (numeric) and *saturate* (boolean) properties.

Examples
--------

    PXN8.tools.lomo({opacity: 30, saturate: true});

<table>
<tr><td>Before</td><td>After</td></tr>
<tr><td><img src="pigeon300x225.jpg"/></td><td><img src="pigeon300x225lomo.jpg"/></td></tr>
</table>

***/
PXN8.tools.lomo = function(params)
{
    params.operation = "lomo";
    PXN8.tools.updateImage([params]);
};

/**************************************************************************

PXN8.tools.fill_flash()
=======================
Adds a fill-flash effect to the photo to brighten it. This is more
subtle than using the PXN8.tools.colors() function as it composites a
duplicate layer on top of the existing image using the 'SCREEN'
compositing operation.

Parameters
----------
* luminosity : A value between 1 and 100 (default value is 50 if parameter not passed).

Examples
--------
    
    PXN8.tools.fill_flash();

<table>
<tr><td>Before</td><td>After</td></tr>
<tr><td><img src="pigeon300x225.jpg"/></td><td><img src="pigeon300x225fillflash.jpg"/></td></tr>
</table>

***/
PXN8.tools.fill_flash = function(opacity)
{
    var operation = {operation: "fill_flash"};
    if (opacity){
        operation.opacity = Math.max(0,Math.min(100,opacity));
    }else{
        operation.opacity = 50;
    }
    
    PXN8.tools.updateImage([operation]);
};

/**************************************************************************

PXN8.tools.snow()
=================
Adds snowflakes to the photo. This is basically a wrapper around PXN8.tools.overlay().

Examples
--------

    PXN8.tools.snow();

<table>
<tr><td>Before</td><td>After</td></tr>
<tr><td><img src="pigeon300x225.jpg"/></td><td><img src="pigeon300x225snow.jpg"/></td></tr>
</table>

Related
-------
PXN8.tools.overlay()

***/
PXN8.tools.snow = function ()
{
    //
    // snowflakes.png must be in the pixenate/images/overlays directory !!!!
    //
    PXN8.tools.overlay({image: "snowflakes.png", tile: "true"});
};

/**************************************************************************

PXN8.tools.overlay
==================
Overlays an image on top of the photo.
The overlay tool is useful for superimposing clip-art and borders on top of photos.

Parameters
----------

* params : An object with the following properties...
  * image : (compulsory) The image property should be the name of an overlay image (either .png or .gif) stored in the *${pixenate}/images/overlays/* directory (where ${pixenate} is the name of the directory where pixenate is located. This is the image which will be overlaid on top of the user's photo. 
  * tile  : (optional) A boolean indicating whether the image should be tiled (repeated in the x and y direction).
  * x : (optional) The X coordinate where the left-most side of the overlay will appear (ignored if *tile* is true).
  * y : (optional) The Y coordinate where the top-most side of the overlay will appear (ignored if *tile* is true).
  * width: (optional) The width to which the overlay will be resized. If the width property is not present then the overlay will not be resized.
  * height: (optional) The height to which the overlay will be resized.

Examples
--------

Please see the <a href="example-speech-bubbles.html">Speech bubbles</a> Example.

Related
-------
PXN8.tools.snow() PXN8.tools.add_text()

***/
PXN8.tools.overlay = function(params)
{
    params.operation="overlay";
    PXN8.tools.updateImage([params]);
};


/**************************************************************************

PXN8.tools.add_text()
=====================
Adds text to a photo.

Parameters
----------

* params : An object with the following properties...
  * text : (this is the only compulsory property) The text to add.
  * gravity : A string specifying where the text will appear. Possible values are "North", "South", "East", "West", "NorthWest", "SouthWest", "NorhtEast", "SouthEast", "Center".
  * top : The Y coordinate where the text will appear (ignored if *gravity* is present).
  * left : The X coordinate where the text will appear (ignored if *gravity* is present).
  * width : The width of the text (text will be resized to fit the width).
  * height: The height of the text (text will be resized to fit the height).
  * font : The font family to use when adding text (default is Arial or Courier depending on what fonts are installed on the server).
  * fill : The font color (expressed as a &hash prefixed hexadecimal string).
  * pointsize: The size of the font.

Examples
--------
To position text on the photo using the *gravity* property...

    PXN8.tools.add_text({fill : '#ffffff', font: 'Arial', pointsize: 20, text: 'Hello World', gravity: 'NorthEast'});

<img src="pigeon300x225textgravity.jpg"/>

To position text on the photo using the left and top coordinates...

    PXN8.tools.add_text({fill : '#ffffff', font: 'Arial', pointsize: 20, text: 'Hello World', top: 40, left: 50});

<img src="pigeon300x225textxy.jpg"/>

Related
-------
PXN8.tools.overlay()

***/
PXN8.tools.add_text = function(params)
{
   params.operation = "add_text";
   params.fill = escape(params.fill);
   
   //
   // wph 20070309 : allow doublequotes inside text string
   //
   params.text = params.text.replace(/\"/g,"\\\"");
   
   PXN8.tools.updateImage([params]);
};

/**************************************************************************

PXN8.tools.whiten()
===================
Whitens off-color teeth. 

Parameters
----------
* geometry : An object containing *top*, *left*, *width* and *height* coordinates. (usually obtained from the current user selection)

Related
-------
PXN8.tools.fixredeye()

***/
PXN8.tools.whiten = function (params)
{
    params.operation = "whiten";
    PXN8.tools.updateImage([params]);
};

/**************************************************************************

PXN8.tools.fixredeye()
======================
Removes 'red-eye' from the specified area.

Parameters
----------
* geometry : An object containing *top*, *left*, *width* and *height* coordinates. (usually obtained from the current user selection)


Examples
--------

    PXN8.tools.fixredeye({top: 40, left: 60, width: 75, height: 75});

Alternatively the *geometry* parameter can instead be an Array of geometry objects.

    PXN8.tools.fixredeye([{top: 40, left: 60, width: 75, height: 75},
                         {top: 50, left: 200, width: 80, height: 94}]);

You must supply one or more rectangles to which the redeye fix will be applied.
The rectangles should be approximately centered on the eye.

Related
-------
PXN8.tools.whiten()

***/
PXN8.tools.fixredeye = function(params)
{
    if (!params){
        params = PXN8.getSelection();
    }
    if (PXN8.isArray(params)){
        for (var i = 0;i < params.length; i++){
            params[i].operation = "fixredeye";
        }
        PXN8.tools.updateImage(params);
    }else{
        params.operation = "fixredeye";
        PXN8.tools.updateImage([params]);
    }
};

/**************************************************************************

PXN8.tools.resize()
===================
Resize an image to the specified width and height.

Parameters
----------
* width : The new desired width of the image.
* height: The new desired height of the image.

***/
PXN8.tools.resize = function(width, height)
{
    PXN8.tools.updateImage([{"operation": "resize", "width": width, "height": height}]);
};

/**************************************************************************

PXN8.tools.roundedcorners()
===========================
Add rounded corners to the image.

Parameters
----------
* color : The color of the corners. 
* radius : The radius of the rounded corners.

Examples
--------

    PXN8.tools.roundedcorners('#ffffff',32);

<table>
<tr><td>Before</td><td>After</td></tr>
<tr><td><img src="pigeon300x225.jpg"/></td><td><img src="pigeon300x225rounded.jpg"/></td></tr>
</table>

***/
PXN8.tools.roundedcorners = function(color, radius)
{
    PXN8.tools.updateImage([{"operation":"roundcorners",
                             "color": escape(color), 
                             "radius":radius}]);
};

/**************************************************************************

PXN8.tools.sepia()
==================
Add a sepia-tone effect to the image.

Parameters
----------
* color : the color of the 'tint' to use when applying the effect. ('#a28a65' seems to be a good sepia color).

Examples
--------

    PXN8.tools.sepia('#a28a65');

<table>
<tr><td>Before</td><td>After</td></tr>
<tr><td><img src="pigeon300x225.jpg"/></td><td><img src="pigeon300x225sepia.jpg"/></td></tr>
</table>

Related
-------
PXN8.tools.grayscale()

***/
PXN8.tools.sepia = function(color)
{
    PXN8.tools.updateImage([{"operation":"sepia",
                             "color": escape(color)}]);
};

/**************************************************************************

PXN8.tools.grayscale()
======================
Make the image grayscale (black & white).

Examples
--------
    PXN8.tools.grayscale();

<table>
<tr><td>Before</td><td>After</td></tr>
<tr><td><img src="pigeon300x225.jpg"/></td><td><img src="pigeon300x225grayscale.jpg"/></td></tr>
</table>

Related
-------
PXN8.tools.sepia()

***/
PXN8.tools.grayscale = function()
{
    PXN8.tools.updateImage([{"operation":"grayscale"}]);
};

/**************************************************************************

PXN8.tools.charcoal()
=====================
Create a charcoal drawing from an image.

Parameters
----------
* radius : (A value between 1 and 8). Defines how acute the effect will be. 

Examples
--------

<table>
<tr><td>Before</td><td>radius = 2</td><td>radius = 5</td></tr>
<tr><td><img src="pigeon300x225.jpg"/></td><td><img src="pigeon300x225ch2.jpg"/></td><td><img src="pigeon300x225ch5.jpg"/></td></tr>
</table>

Related
-------
PXN8.tools.oilpaint()
***/
PXN8.tools.charcoal = function(radius)
{
    PXN8.tools.updateImage([{"operation" : "charcoal", "radius" : radius}]);
};

/**************************************************************************

PXN8.tools.oilpaint()
=====================
Create an oil-painting from a photo.

Parameters
----------
* radius : (A value between 1 and 8). Defines how acute the effect will be. 

Examples
--------

<table>
<tr><td>Before</td><td>radius = 2</td><td>radius = 5</td></tr>
<tr><td><img src="pigeon300x225.jpg"/></td><td><img src="pigeon300x225oil2.jpg"/></td><td><img src="pigeon300x225oil5.jpg"/></td></tr>
</table>

Related
-------
PXN8.tools.charcoal()

***/
PXN8.tools.oilpaint = function(radius)
{
    PXN8.tools.updateImage([{"operation" : "oilpaint", "radius" : radius}]);
};

/*========================================================================

PXN8.tools.unsharpmask()
========================
Uses the unsharpmask algorithm to sharpen an image.

*/
PXN8.tools.unsharpmask = function(params)
{
    var operation = {"operation": "unsharpmask"};
    if (params){
        for (var i in params){
            operation[i] = params[i];
        }
    }
    PXN8.tools.updateImage([operation]);
};

/**************************************************************************

PXN8.tools.fetch()
==================
Fetches an image either from a remote server or the server's own filesystem.
This is not normally called directly by client code.

***/
PXN8.tools.fetch = function(params)
{
    var operation = {"operation" : "fetch"};
    if (params){
        for (var i in params){
            operation[i] = params[i];
        }
    }
    PXN8.tools.updateImage([operation]);
};

/* ============================================================================
 *
 * (c) Copyright SXOOP Technologies Ltd. 2005-2006
 * All rights reserved.
 *
 * This file contains code for handling hi-res images
 *
 */

var PXN8 = PXN8 || {};
// called when the hi-res update is complete
PXN8.ON_HIRES_COMPLETE = "ON_HIRES_COMPLETE";
PXN8.ON_HIRES_BEGIN = "ON_HIRES_BEGIN";


PXN8.hires = {
    originalURL: "",
    responses : [],
    jsonCallback : function(jsonResponse){
        PXN8.listener.notify(PXN8.ON_HIRES_COMPLETE,jsonResponse);
    }
};
/**
 * Given a series of commands, scale each of the commands to a certain ratio
 * Only certain commands need to be scaled
 * Any command with 'top','left','width','height' or 'radius' parameters needs 
 * to be scaled.
 */
PXN8.hires.scaleScript = function(script,ratio)
{
    var paramsToScale = ["left","width","top","height","radius"];

    for (var i = 0;i < script.length; i++){
        var op = script[i];
        for (var j = 0; j < paramsToScale.length; j++){
            var attr = paramsToScale[j];
            
            if (op[attr]){
                op[attr] = op[attr] * ratio;
            }
        }
    }
};

/**
 * Called whenever the image is updated by the user.
 */
PXN8.hires.doImageChange = function(eventType)
{
    var loRes = PXN8.images[0];
    var ratio = PXN8.hires.responses[0].height / loRes.height;
    
    var script = PXN8.getScript();

    PXN8.hires.interpolate(script,PXN8.hires.originalURL,ratio);
    
};
/**
 * Apply a script which was performed on a lo-res version of the image
 * to the high-res version of the same image.
 * Parameters : originalScript - the Original Script that was used on the 
 * the lo-res version (see PXN8.getScript() )
 *  hiresImageURL - the URL to the hi-res version of the same image
 *  ratio  - the hires height divided by the lo-res height
 *   (e.g. if the hires image is 3000x2000 and the lo-res image is 600x400
 *   the ratio will be 5 )
 */
PXN8.hires.interpolate = function(originalScript,hiresImageURL,ratio)
{
    originalScript[0].image = hiresImageURL;

    PXN8.hires.scaleScript(originalScript,ratio);
    
    PXN8.log.append("about to submit: "+ PXN8.objectToString(originalScript));
        
    PXN8.listener.notify(PXN8.ON_HIRES_BEGIN);

    var opNumberWas = PXN8.opNumber;
    
    PXN8.ajax.submitScript(originalScript,function(jsonResponse){
        //
        // wph 20070228 : curry so the callback will know which opNumber
        // it was called for (may not be what the current value of PXN8.opNumber is 
        // when this is eventually invoked!)
        //
        jsonResponse.initOpNumber = opNumberWas;
        PXN8.hires.jsonCallback(jsonResponse);
        
    });
};


/**
 * Initialize the Hi-Res Ajax Requestor
 * This will kick off a listener which will request an updated version of the hi-res image
 * whenever the user changes the lo-res version.
 */
PXN8.hires.init = function(imageUrl)
{
    PXN8.listener.add(PXN8.ON_HIRES_COMPLETE,function(eventType,jsonResponse){
        PXN8.hires.responses[jsonResponse.initOpNumber] = jsonResponse;
        PXN8.log.append("hires: "+ PXN8.objectToString(jsonResponse));
    });
    // set so that later calls will use same URL
    PXN8.hires.originalURL = imageUrl;
    
    var fetch = {operation: "fetch",
                 image: imageUrl,
                 pxn8root: PXN8.root,
                 random: Math.random()
    };
    PXN8.listener.notify(PXN8.ON_HIRES_BEGIN);

    var opNumberWas = PXN8.opNumber;
    
    PXN8.ajax.submitScript([fetch],function(jsonResponse){
        //
        // wph 20070228 : curry so the callback will know which opNumber
        // it was called for (may not be what the current value of PXN8.opNumber is !)
        //
        jsonResponse.initOpNumber = opNumberWas;
        PXN8.hires.jsonCallback(jsonResponse);
    });
    
    
    PXN8.listener.add(PXN8.ON_IMAGE_CHANGE,PXN8.hires.doImageChange);
    /**
     * over-ride the default PXN8.getUncompressedImage if in hi-res mode
     */
    PXN8.getUncompressedImage = PXN8.hires.getUncompressedImage;
};

/**
 * Get the path to the uncompressed hi-res edited image
 */
PXN8.hires.getUncompressedImage = function()
{
    var result = false;
    if (PXN8.hires.responses[PXN8.opNumber]){
        result = PXN8.hires.responses[PXN8.opNumber].uncompressed;
    }
    return result;
    
};

/* ============================================================================
 *
 * (c) Copyright SXOOP Technologies Ltd. 2005-2006
 * All rights reserved.
 *
 * This file contains most of the string literals used by PXN8's UI
 *
 */

var PXN8 = PXN8 || {};

PXN8.strings = {};

// alert when no more redo
PXN8.strings.NO_MORE_REDO = "No more operations left to redo!";

// alert when no more undo 
PXN8.strings.NO_MORE_UNDO     = "No operations left to undo!";


// alert when fully zoomed in
PXN8.strings.NO_MORE_ZOOMIN  = "Cannot zoom-in any further!";

    // alert when fully zoomed out
PXN8.strings.NO_MORE_ZOOMOUT      = "Cannot zoom-out any further!";

    // alert when using old IE (pre 6.0)
PXN8.strings.MUST_UPGRADE_IE      = "You must upgrade to Internet Explorer 6.0 to use PXN8";

    // alert when AJAX request fails due to server error
PXN8.strings.WEB_SERVER_ERROR     = "Web server error:";

    // alert when an image fails to load due to bad URL
PXN8.strings.IMAGE_ON_ERROR1      = "An error occured while attempting to load ";

PXN8.strings.IMAGE_ON_ERROR2       = "\nPlease check the URL is correct and try again";

    // alert when no pxn8_config_content div has been defined
PXN8.strings.NO_CONFIG_CONTENT    = "ERROR: no config_content element is defined in your html template";
    
PXN8.strings.CONFIG_BLUR_TOOL     = "Configure Blur tool";
	
    // appears at the bottom of the blur config tool
PXN8.strings.BLUR_PROMPT          = "Enter a value in the range 1 to 8 for blur radius. A larger radius results in a more blurred image.";

    // alert when blur out of range
PXN8.strings.BLUR_RANGE           = "Blur radius must be in the range 1 - 8";

PXN8.strings.RADIUS_LABEL         = "Radius:";
	
    // alert when brightness out of range
PXN8.strings.BRIGHTNESS_RANGE     = "Enter a percentage value for brightness";

    // alert when hue out of range
PXN8.strings.HUE_RANGE            = "Hue must be in the range 0-200";

    // alert when saturation out of range
PXN8.strings.SATURATION_RANGE     = "Enter a percentage value for saturation";

    // appears at the top of the crop tool panel
PXN8.strings.CONFIG_CROP_TOOL     = "Configure Crop Tool";

PXN8.strings.CONFIG_COLOR_TOOL    = "Change colors ";
    
    // appears at the top of the lens filter tool panel
PXN8.strings.CONFIG_FILTER_TOOL   = "Configure Lens Filter";

    // appears at the bottom of the blur config tool.
PXN8.strings.FILTER_PROMPT        = "Click on the image and a graduated filter of the selected color (and opacity) will be overlayed on top of the image";

    // appears at the top of the interlace tool panel
PXN8.strings.CONFIG_INTERLACE_TOOL= "Configure Interlace Effect";
    
PXN8.strings.INTERLACE_PROMPT      = "Creates an interlaced overlay above the image making it appear like a grab from a TV screen.";

PXN8.strings.INVALID_HEX_VALUE     = "You must enter a hexadecimal color value or choose one from the color palette";

PXN8.strings.CONFIG_LOMO_TOOL      = "Configure Lomo Effect";

PXN8.strings.OPACITY_PROMPT        = "Low opacity means darker corners. High opacity means lighter corners.";

PXN8.strings.OPACITY_RANGE         = "Opacity must be in the range 0 - 100";

PXN8.strings.WHITEN_SELECT_AREA    = "You must first select the area of the image you wish to whiten";

PXN8.strings.CONFIG_WHITEN_TOOL    = "Configure Teeth Whitening";

PXN8.strings.CROP_SELECT_AREA      = "You must first select the area of the image you wish to crop";    

PXN8.strings.RESIZE_SELECT_AREA    = "You must first select an area to resize to.";

PXN8.strings.RESIZE_SELECT_LABEL   = "Resize to selected area.";

PXN8.strings.SELECT_SMALLER_AREA   = "Please select a smaller area";

PXN8.strings.REDEYE_SELECT_AREA    = "You must first select the area you wish to fix";
    
PXN8.strings.REDEYE_SMALLER_AREA   = "Please select a smaller area to fix (less than 100x100)";

PXN8.strings.CONFIG_REDEYE_TOOL    = "Fix Red Eye";

PXN8.strings.REDEYE_PROMPT         = "To fix red-eye, select the affected area (begining at the top left corner and centering the cross-hairs on the iris) and click the 'Apply' button or press 'Enter'.";
    
PXN8.strings.NUMERIC_WIDTH_HEIGHT  = "You must specify a numeric value for new width and height";
    
PXN8.strings.LIMIT_SIZE            = "You can't resize larger than ";
    
PXN8.strings.ASPECT_LABEL          = "Preserve aspect ratio: ";

PXN8.strings.ASPECT_CROP_LABEL     = "Aspect ratio: ";

PXN8.strings.CROP_FREE             = "free select";

PXN8.strings.CROP_SQUARE           = "(square)";

PXN8.strings.WIDTH_LABEL           = "Width: ";

PXN8.strings.HEIGHT_LABEL          = "Height: ";

PXN8.strings.FLIPVT_LABEL          = "Flip vertically: ";

PXN8.strings.FLIPHZ_LABEL          = "Flip horizontally: ";

PXN8.strings.ANGLE_LABEL           = "Angle: ";
	
PXN8.strings.OPACITY_LABEL         = "Opacity: ";
 
PXN8.strings.CONTRAST_NORMAL       = "Normal ";
 
PXN8.strings.COLOR_LABEL           = "Color: ";

PXN8.strings.SEPIA_LABEL           = "Sepia";

PXN8.strings.SATURATE_LABEL        = "Saturate :";

PXN8.strings.GRAYSCALE_LABEL       = "Grayscale";

PXN8.strings.ORIENTATION_LABEL     = "Orientation: ";

PXN8.strings.CONFIG_RESIZE_TOOL    = "Resize Image";
    
PXN8.strings.CONFIG_ROTATE_TOOL    = "Rotate or Flip Image";
    
PXN8.strings.SPIRIT_LEVEL_PROMPT1  = "Please click on the left half of the crooked horizon.";

PXN8.strings.SPIRIT_LEVEL_PROMPT2  = "OK. Now click on the right half of the crooked horizon.";

PXN8.strings.CONFIG_SPIRITLVL_TOOL = "Spirit-level Mode";
    
PXN8.strings.CONFIG_ROUNDED_TOOL   = "Configure Rounded Corners";

PXN8.strings.CONFIG_BW_TOOL        = "Configure Sepia or Black & White";

PXN8.strings.ORIENTATION_PORTRAIT  = "Portrait";

PXN8.strings.ORIENTATION_LANDSCAPE = "Landscape";

PXN8.strings.PROMPT_ROTATE_CHOICE  = "Please specify an angle of rotation or flip orientation";
    
PXN8.strings.BW_PROMPT             = "Turn your photograph into black & white or add a sepia tone.";

PXN8.strings.IMAGE_UPDATING        = "The image is currently updating.\nPlease wait for the current operation to complete.";

PXN8.strings.BRIGHTNESS_LABEL       = "brightness";

PXN8.strings.SATURATION_LABEL       = "saturation";

PXN8.strings.CONTRAST_LABEL       = "contrast";

PXN8.strings.HUE_LABEL       = "hue";
	
PXN8.strings.UPDATING        = "Updating image. Please wait...";

PXN8.strings.CONFIG_OILPAINT_TOOL    = "Apply Oil-Painting Filter";

PXN8.strings.CONFIG_CHARCOAL_TOOL    = "Apply Charcoal Filter";




	
/* ============================================================================
 *
 * (c) Copyright SXOOP Technologies Ltd. 2005-2007
 * All rights reserved.
 *
 * This file contains code for displaying cross-hairs on the selection
 *
 */
var PXN8 = PXN8 || {};

PXN8.crosshairs = {};
/*************************************************************************

SECTION: Cross-Hairs functions
==============================
The following functions are related to setting and getting the cross-hairs image
(by default a white cross which is displayed in the center of the selection area),
and also enabling and disabling the cross-hairs.

***/

PXN8.crosshairs.enabled = true;
PXN8.crosshairs.image = null;


/**************************************************************************

PXN8.crosshairs.setEnabled()
============================
Enables or disables the display of the cross-hairs image at the center of the 
selected area.

Parameters
----------

* enabled : A boolean (true or false) indicating whether or not cross-hairs should be displayed.

Related
-------
PXN8.crosshairs.isEnabled()

***/
PXN8.crosshairs.setEnabled = function(enabled){
    PXN8.crosshairs.enabled = enabled;
    PXN8.crosshairs.refresh();
};

/**************************************************************************

PXN8.crosshairs.isEnabled()
===========================
Returns a boolean value indicating whether or not the crosshairs are displayed in the selected area.

Returns
-------
*true* if the crosshairs are displayed in selections. *false* otherwise.

Related
-------
PXN8.crosshairs.setEnabled()

***/
PXN8.crosshairs.isEnabled = function(){
    return PXN8.crosshairs.enabled;
};



/**************************************************************************

PXN8.crosshairs.getImage()
==========================
Get the Image URL currently used for displaying the cross-hairs at the center
of the selected area.

Returns
-------
A URL to the image used as the cross-hairs image. The URL returned will be an 
absolute URL including the domain name and full URL path.

Related
-------
PXN8.crosshairs.setImage()

***/
PXN8.crosshairs.getImage = function()
{
    if (PXN8.crosshairs.image == null){
        PXN8.crosshairs.image = PXN8.server + PXN8.root + "/images/pxn8_xhairs_white.gif";
    }
    return PXN8.crosshairs.image;
};

/***************************************************************************

PXN8.crosshairs.setImage()
==========================
You can change the cross-hairs image to suit your own tastes.

Parameters
----------
* imageURL : A URL to the image which should be displayed in the center of the selection area.

Related
-------
PXN8.crosshairs.getImage()

***/
PXN8.crosshairs.setImage = function(imageURL)
{
    PXN8.crosshairs.image = imageURL;
    PXN8.crosshairs.refresh();
};

/**
 * Called when the selection changes
 */
PXN8.crosshairs.refresh = function()
{
    var _ = PXN8.dom;
    var xhairs = _.id("pxn8_crosshairs");

    if (!PXN8.crosshairs.isEnabled()){
        if (xhairs){
            xhairs.style.display = "none";
        }
        return;
    }
    var sel = PXN8.getSelection();
    var _ = PXN8.dom;
    var selBounds = _.eb("pxn8_select_rect");
    var canvas = _.id("pxn8_canvas");
    if (!xhairs){
        xhairs = _.ac(canvas,_.ce("img",{id: "pxn8_crosshairs", src: PXN8.crosshairs.getImage()}));
    }
    if (selBounds.width <= 0){
        xhairs.style.display = "none";
        return;
    }
    //
    // center the crosshairs on the selection area
    //
    var xhcx = xhairs.width/2;
    var xhcy = xhairs.height/2;
    
    xhairs.style.display = "inline";
    xhairs.style.position = "absolute";
    //
    // wph 20070613 : must use Math.floor or the xhairs will not be perfectly centered 
    // (off by up to 2 pixels to the right and bottom)
    //
    xhairs.style.left = Math.floor((selBounds.x + (selBounds.width / 2)) - xhcx) + "px";
    xhairs.style.top = Math.floor((selBounds.y + (selBounds.height / 2)) - xhcy) + "px";
};

PXN8.listener.add(PXN8.ON_SELECTION_CHANGE,PXN8.crosshairs.refresh);

/*
 * (c) Copyright SXOOP Technologies Ltd. 2005-2007
 * All rights reserved.
 *
 */
PXN8 = PXN8 || {};
/**************************************************************************

SECTION: Preview Functions
==========================
Pixenate allows you to display a "preview pane" on your page where you can see
a preview of what the currently active tool (lomo, colors, etc) will look like.
Ideally, the preview pane should only show a small section of the entire image.
The user can click and drag inside the preview pane to show obscured parts of the image.

***/

PXN8.preview = {};
PXN8.preview._div_to_intervalid = {};


/**************************************************************************
PXN8.preview.initialize()
=========================
Multiple Preview panes can be displayed at the same time so each distinct preview
pane must be associated with it's own preview state object. The preview state object
just contains information about the state of the preview pane : the relative position of
the background image and other information specific to that particular preview pane.
When you initialize a preview pane, you must provide an Element Id.

Parameters
----------
* ElementId : The ID attribute of the HTML Element (a DIV ideally) in which the preview will be displayed.

Returns
-------
An object which contains the state information for preview-related data for the supplied Element.
This object should be later passed to the PXN8.preview.show() and PXN8.preview.hide() functions.

Examples
--------
The following code will create a preview pane on the page. And will initialize the Preview pane
with a 'colors' operation...

    <div id="colors_preview" style="width: 120px; height: 120px;"></div>
    <script type="text/javascript">
       var colors_peek = PXN8.preview.initialize("colors_preview");
       PXN8.preview.show(colors_peek, {operation: "colors", brightness: 150, saturation: 150});
    </script>

<img src="pigeon300x225preview.jpg"/>

Related
-------
PXN8.preview.show() PXN8.preview.hide() OPERATIONS

***/
PXN8.preview.initialize = function(elementId)
{
    /**
     * It is VERY IMPORTANT that backgroundImageCache is enabled 
     * in IE - otherwise there is an annoying flicker when the preview
     * pane is dragged.
     */
    try {
        document.execCommand('BackgroundImageCache', false, true);
    } catch(e) {}


    /**
     * Firstly, clear any existing event listeners and timers for the element.
     */
    PXN8.event.removeListener(elementId,"mousedown");
    PXN8.event.removeListener(elementId,"mouseup");
    PXN8.event.removeListener(elementId,"mousemove");
    var oldIntervalId = PXN8.preview._div_to_intervalid[elementId];
    if (oldIntervalId){
        clearInterval(oldIntervalId);
    }


    var result = {};
    
    var element = PXN8.dom.id(elementId);
    if (!element){return false;}
    
    result._element = element;
    result._sizeX = parseInt(element.style.width);
    result._sizeY = parseInt(element.style.height);
    result._offset = {x: 0, y: 0};
    result._op = false;
    result._opQueue = [];
    result._intervalId = 0;
    result._beginDrag = {x: 0, y: 0};
    result._endDrag = {x: 0, y: 0};
    result._mouseDownCoords = {};
    result._mouseUpCoords = {};
    
    result._mouseDown = PXN8.event.closure(result,PXN8.preview._innerMouseDown);
    result._mouseUp = PXN8.event.closure(result,PXN8.preview._innerMouseUp);
    result._mouseMove = PXN8.event.closure(result,PXN8.preview._innerMouseMove);


    
    var img = PXN8.dom.id("pxn8_image");
    if (img){
        //
        // pxn8_image might not yet be present when PXN8.preview.initialize() is called
        //
        var real_image_width = img.width / PXN8.zoom.value();
        var real_image_height = img.height / PXN8.zoom.value();
        result._fullsize_ratio = Math.ceil(Math.max(real_image_height/result._sizeY, real_image_width/result._sizeX));
        /**
         * Center the preview pane on the image
         */
        PXN8.preview.centerOffset(result);

    }else{
        result._fullsize_ratio = -1;
    }
    
    result._intervalId = setInterval(function(){PXN8.preview._manageQueue(result);},750);
    
    PXN8.preview._div_to_intervalid[elementId] = result._intervalId;
        
    PXN8.event.addListener(elementId,"mousedown",result._mouseDown);
    

    return result;
};

/**
 * Show a part of the photo in the preview pane
 */
PXN8.preview.setOffset = function(object,x,y)
{
    var img = PXN8.dom.id("pxn8_image");
    var iw = img.width / PXN8.zoom.value();
    var ih = img.height / PXN8.zoom.value();
    
    var maxY = ih - object._sizeY;
    var maxX = iw - object._sizeX;
    if (x < 0){ x = 0; }
    if (y < 0){ y = 0; }
    if (y > maxY) { y = maxY;}
    if (x > maxX) { x = maxX;}
                     
    object._offset.x = x;
    object._offset.y = y;
    PXN8.preview._refresh(object);
};


/**
 * Make the preview pane show the center of the photo
 */
PXN8.preview.centerOffset = function(object)
{
    var _ = PXN8.dom;
    var image = _.id("pxn8_image");
    var iw = image.width / PXN8.zoom.value();
    var ih = image.height / PXN8.zoom.value();
    var halfW = object._sizeX / 2;
    var halfH = object._sizeY / 2;
    PXN8.preview.setOffset(object,Math.round((iw/2) - halfW), Math.round((ih/2) - halfH));
};




/**************************************************************************

PXN8.preview.show()
===================
Shows the preview pane.

Parameters
----------
* previewStateObject : The object which was returned from PXN8.preview.initialize()
* operation : An operation state object. This parameter is *optional* .

The operation parameter can be a single *operation* object (as described in <a href="#OPERATIONS">OPERATIONS</a>
or an array of operation objects.

Example
-------
The following code creates two buttons to increase and decrease the saturation of the photo
and show the change in a preview pane...


    <div id="colors_preview" style="width: 120px; height: 120px;"></div>
    <script type="text/javascript">
       //
       // declare a global variable 'colors_peek' to be used by buttons too.
       //
       colors_peek = PXN8.preview.initialize("colors_preview");
       saturation = 100;

       PXN8.preview.show(colors_peek);

       function increase_saturation(){
          saturation += 20;
          //
          // Update the preview pane.
          //
          PXN8.preview.show(colors_peek,{"operation": "colors", "saturation": saturation});
       }

       function decrease_saturation(){
          saturation -= 20;
          //
          // Update the preview pane.
          //
          PXN8.preview.show(colors_peek,{"operation": "colors", "saturation": saturation});
       }
    </script>

    <button onclick="increase_saturation()">+</button>
    <button onclick="decrease_saturation()">-</button>

<img src="pigeon300x225previewshow.jpg"/>

The following code demonstrates previewing a combined operation (Saturation and Interlace combined)...

    PXN8.preview.show(colors_peek,
		                [{"operation": "colors", "saturation": saturation},
                       {"operation": "interlace", opacity: 50,color: '#ffffff'}             
 		                ]);

Related
-------
PXN8.preview.initialize() PXN8.preview.show() OPERATIONS

***/
PXN8.preview.show = function(object,op)
{
    var _ = PXN8.dom;
    
    if (!object._element){
        return;
    }
    if (object._element.style.display == "none"){
        object._element.style.display = "block";
    }
    if (op){ object._op = op; }

    if (op){ 
        PXN8.preview._enqueue(object); 
    }
};

/**************************************************************************

PXN8.preview.hide()
===================
Hide (and clear) the preview pane. When you're finished with Preview Pane,
you should call this method to clean up.

Parameters
----------
* previewStateObject : The object which was returned from the call to PXN8.preview.initialize()

Examples
--------
Please refer to the <a href="example-preview.html">Preview Example</a>.

Related
-------
PXN8.preview.show() PXN8.preview.initialize() OPERATIONS

***/
PXN8.preview.hide = function(object)
{
    if (object._element){
        object._element.innerHTML = "";
        object._element.style.display = "none";
    }
    object._op = false;
    clearInterval(object._intervalId);

    PXN8.event.removeListener(object._element,"mousedown",object._mouseDown);
    PXN8.event.removeListener(object._element,"mousemove",object._mouseMove);
    PXN8.event.removeListener(object._element,"mouseup",object._mouseUp);
};


PXN8.preview._enqueue = function(object)
{
    object._opQueue.push(object._op);
};
/**
 * A global used by all preview panes.
 * don't throttle the server with too many spurious previews
 * If the last preview op hasn't completed then wait until it's complete before
 * submitting the next preview op.
 */
PXN8.preview._complete = true;

/**
 * description: Which part of the photo is the preview pane showing ?
 */
PXN8.preview._getOffset = function(object)
{
    return {x: object._offset.x, y: object._offset.y};
};

PXN8.preview._manageQueue = function(object)
{
    //
    // a last-in first-out queue.
    // all but the last op are ignored.
    // (this is to avoid excessive calls to the server).
    //
    var lastIndex = object._opQueue.length -1;
    if (lastIndex < 0){
        // nothing on the queue 
        return;
    }

    if (!PXN8.preview._complete){
        return;
    }
    //
    // The pxn8_preview div uses the current image's src as it's backgroundImage anyway
    // so no need to go to the server unless there's an op to perform
    //
    var _ = PXN8.dom;
    var script = PXN8.getScript();
    var image = _.id("pxn8_image");
    if (!image){
        return;
    }
    
    //
    // set the _complete flag now
    //
    PXN8.preview._complete = false;

    var op = object._opQueue[lastIndex];
    PXN8.log.append("There are " + object._opQueue.length + " elements in queue");
    object._opQueue.length = 0;
    
    
    var width = image.width / PXN8.zoom.value();
    var height = image.height / PXN8.zoom.value();
    
    var top = object._offset.y;
    var left = object._offset.x;
    //
    // round the x & y offsets to the nearest 100 pixels
    // to avoid too much server interaction
    // (snap-to-grid for less granular calls to server )
    //
    var gridSize = 100;

    var offsetTop = top % gridSize;
    var offsetLeft = left % gridSize;
    
    top = top - offsetTop;
    left = left - offsetLeft;

    var right = Math.min(width,left + (object._sizeX + gridSize));
    var bottom = Math.min(height, top + (object._sizeY + gridSize));

    var addedOps = [];
    
    var cropOp = {"operation":"crop", "top":top, "left":left, "width":right-left, "height":bottom-top, "__quality":100, "__uncompressed":0};
    
    if (width > object._sizeX && height > object._sizeY){
        addedOps.push(cropOp);
    }
    var opsFromQueue = [];
    if (PXN8.isArray(op)){
        opsFromQueue = op;
    }else{
        opsFromQueue.push(op);
    }
    
    for (var i = 0;i < opsFromQueue.length; i++){
        var opFromQueue = opsFromQueue[i];
        
        /**
         * For the unsharpmask operation, the quality is very important
         * Only set the quality to 65 if a __quality attribute is not already present
         */
        if (opFromQueue["__quality"] == null){
            opFromQueue.__quality = 65;
        }
        opFromQueue.__uncompressed = 0;
        
        addedOps.push(opFromQueue);
    }
    
    
    object._element.innerHTML = '<span class="pxn8_preview_update">Please wait...</span>';

    
    var cachedImage = PXN8.getUncompressedImage();
    if (cachedImage){
        // 
        // truncate script so it can be used for GET requests
        //
        script = [{"operation": "cache", "image":cachedImage}];
    }
    for (var i = 0;i < addedOps.length; i++){
        script.push(addedOps[i]);
    }

    PXN8.ajax.submitScript(script, function(jsonResponse){
        PXN8.preview._complete = true;
        //
        // if the server returned an error...
        // (such errors - JSON eval failures - get reported in the PXN8.ajax module
        // anyway)
        _.cl(object._element);

        if (!jsonResponse){
            return;
        }
        var prevImgURL = PXN8.server + PXN8.root + "/" + jsonResponse.image;
        object._element.style.backgroundImage = "url(" + prevImgURL + ")";
        var position = (offsetLeft * -1) + "px " + (offsetTop * -1) + "px";
        object._element.style.backgroundPosition = position;
    });
};

/**
 * Update the preview pane to reflect the current part of the photo showing.
 */
PXN8.preview._refresh = function(object)
{
    var _ = PXN8.dom;

    if (!object._element){
        return;
    }

    var position = (object._offset.x * -1) + "px " + (object._offset.y * -1) + "px";

    var image = _.id("pxn8_image");
    object._element.style.backgroundPosition = position;
    object._element.style.backgroundImage = "url(" + image.src + ")";
    object._element.style.cursor = "move";
    object._element.style.backgroundRepeat = "no-repeat";
};

PXN8.preview._innerMouseMove = function(event,object,source)
{
    var _ = PXN8.dom;

    object._endDrag = _.cursorPos(event);
    var offset = PXN8.preview._getOffset(object);
    var xdiff = object._beginDrag.x - object._endDrag.x;
    var ydiff = object._beginDrag.y - object._endDrag.y;

    if (object._fullsize_ratio == -1){
        var img = PXN8.dom.id("pxn8_image");
        if (img){
            var real_image_width = img.width / PXN8.zoom.value();
            var real_image_height = img.height / PXN8.zoom.value();
            object._fullsize_ratio = Math.ceil(Math.max(real_image_height/object._sizeY, real_image_width/object._sizeX));
        }
    }
    
    offset.x += xdiff * object._fullsize_ratio;
    offset.y += ydiff * object._fullsize_ratio;

    PXN8.preview.setOffset(object,offset.x,offset.y);
    object._beginDrag = object._endDrag;
    
};


PXN8.preview._innerMouseUp = function(event,object,source)
{
    PXN8.event.removeListener(object._element,"mouseup",object._mouseUp);
    PXN8.event.removeListener(object._element,"mouseout",object._mouseUp);
    PXN8.event.removeListener(object._element,"mousemove",object._mouseMove);
    PXN8.preview.show(object,object._op);
};


PXN8.preview._innerMouseDown = function(event,object,source)
{
    var _ = PXN8.dom;

    var img = _.id("pxn8_image");
    object._element.style.backgroundImage = "url(" + img.src + ")";


    PXN8.preview._refresh(object);
    
    object._beginDrag = _.cursorPos(event);
    object._mouseDownCoords = _.cursorPos(event);

    
    PXN8.event.addListener(object._element,"mouseup",object._mouseUp);
    PXN8.event.addListener(object._element,"mouseout",object._mouseUp);
    PXN8.event.addListener(object._element,"mousemove",object._mouseMove);
};


/**
 * (c) Copyright Sxoop Technologies Ltd. 2005 - 2007
 * For handling overlay image movement and resizing
 */

var PXN8 = PXN8 || {};
/***************************************************************************

SECTION: Overlay Helper Functions
=================================
Pixenate provides 2 functions to simplify UI programming for overlaying photos.
PXN8.overlay.start() puts the editor in *overlay* mode. In this mode, every time
the user makes or changes a selected area of the image, an overlay image will
be placed on top of the selected area. The overlay image will be resized to fit the selected
area. This function provides a handy way for users to *preview* what the the overlay will
look like when it is applied.
PXN8.overlay.stop() makes the editor exit *overlay* mode.

***/
PXN8.overlay = {};
/**************************************************************************

PXN8.overlay.start()
====================
PXN8.overlay.start() puts the editor into *overlay* mode. In this mode, an overlay image
appears superimposed on top of the photo whenever the selection is changed. This function
is provided as a convenience function to let users preview  the effects of the 
<a href="#pxn8_tools_overlay">PXN8.tools.overlay()</a> function before applying it. 
Please note that PXN8.overlay.start() does not change the photo. You still need to call PXN8.tools.overlay()
to *apply* the overlay.

Parameters
----------
* overlayURL : A url to the image to use as an overlay. This URL will be used to construct a new image so it should be an Image URL.
* dimensions : An object with *top*, *left*, *width* and *height* properties used to position the overlay image. The dimensions are relative to the top left corner of the <a href="#pxn8_canvas">pxn8_canvas</a> div.

Examples
--------
Please see the <a href="example-speech-bubbles.html">Speech bubbles</a> Example.

Related
-------
PXN8.overlay.stop()

***/
PXN8.overlay.start = function(overlayURL,dimensions) {
    PXN8.overlay.stop();
    
    var rects = ["pxn8_top_rect","pxn8_bottom_rect","pxn8_left_rect","pxn8_right_rect"];
    for (var i = 0;i < rects.length; i++){
        PXN8.dom.opacity(rects[i],0);
    }
    
    var img = document.getElementById("pxn8_image");
    var iw = img.width / PXN8.zoom.value();
    var ih = img.height / PXN8.zoom.value();

    if (!dimensions.top && !dimensions.left){
        dimensions.top = (ih/2) - (dimensions.height / 2);
        dimensions.left = (iw/2) - (dimensions.width / 2);
    }
    
    PXN8.select(dimensions);

    /**
     * create the overlay div if it's not already present
     */
    var overlayEl = PXN8.dom.id("pxn8_overlay");
    if (overlayEl == null){
        overlayEl = PXN8.dom.ce("img", {id: "pxn8_overlay"});
        overlayEl.style.position = "absolute";
        var canvas = PXN8.dom.id("pxn8_canvas");
        canvas.appendChild(overlayEl);
        
    }
    overlayEl.src = overlayURL;
    
    PXN8.overlay.show();

    PXN8.listener.add(PXN8.ON_SELECTION_CHANGE,PXN8.overlay.show);
};
/**************************************************************************

PXN8.overlay.stop()
===================
Makes the editor exit *overlay* mode. You should call this function when the user applies or cancels 
an overlay operation.

Examples
--------
Please see the <a href="example-speech-bubbles.html">Speech bubbles</a> Example.

Related
-------
PXN8.overlay.start()

***/
PXN8.overlay.stop = function(){
    var rects = ["pxn8_top_rect","pxn8_bottom_rect","pxn8_left_rect","pxn8_right_rect"];
    for (var i = 0;i < rects.length; i++){
        PXN8.dom.opacity(rects[i],PXN8.style.notSelected.opacity);
    }
    PXN8.listener.remove(PXN8.ON_SELECTION_CHANGE,PXN8.overlay.show);
    var overlay = document.getElementById("pxn8_overlay");
    /* it's possible the overlay element doesn't exist yet - if called from PXN8.overlay.start */
    if (overlay){
        overlay.style.display = "none";
    }
    PXN8.selectByRatio("free");
    /*
     * don't unselect 
    PXN8.unselect();
     */
    
};

PXN8.overlay.show = function (){

    var overlay = document.getElementById("pxn8_overlay");

    var sel = PXN8.getSelection();
    var zoom = PXN8.zoom.value();
    
    overlay.style.display = "block";
    
    overlay.style.top = (sel.top * zoom) + "px";
    overlay.style.left = (sel.left * zoom) + "px";

    overlay.width = sel.width * zoom;
    overlay.height = sel.height * zoom;
};
/* ============================================================================
 *
 * (c) 2005-2006 Sxoop Technologies Ltd. All rights reserved.
 *
 * For support contact support@sxoop.com
 *
 * These function handle sliders as used by some of the 
 * tool configuration panels.
 */
var PXN8 = PXN8 || {};
PXN8.slide = {};
/**
 * Turn a regular HTML div element into a slide
 * slideElement = The empty DIV that will be turned into a slide
 * inputElementId = The INPUT field which will be updated when the user moves the slider
 * startRange = The smallest valid value 
 * rangeSize = The size of the range (range = startSize + rangeSize)
 * so for example if you want to create a slider with range 80 ... 130 startRange is 80, and rangeSize is 50
 * initValue = The start / default value for the slider & input 
 * optionalCallback = An optional function which gets called whenever the user moves the slider or 
 * the user changes a value in the INPUT field.
 */
PXN8.slide.bind = function(slideElement,inputElementId,startRange,rangeSize,initValue,increment,optionalCallback)
{
    if (!increment){
        increment = 1;
    }
    
    if (typeof slideElement == 'string'){
        slideElement = PXN8.dom.id(slideElement);
    }
    
    slideElement.className = "pxn8_slide";
    slideElement.onmousedown = function(event){
        if (!event) event = window.event;
        PXN8.slide.onmousedown(slideElement,event,inputElementId,startRange,rangeSize,increment);
    };
    var slider = document.createElement("span");
    slider.className = "pxn8_slider";
    slideElement.appendChild(slider);

    PXN8.slide.refresh_slider(slider,initValue,startRange,rangeSize);

    var inputElement = PXN8.dom.id(inputElementId);
    if (!inputElement){
        alert("ERROR: NO <input/> element was found with id=\"" + inputElementId + "\"");
        return false;
    }
    
    inputElement.value = initValue;
    inputElement.onblur = function(){
        if (isNaN(this.value)){
            this.value = startRange;
        }
        if (this.value > startRange + rangeSize){
            this.value = startRange + rangeSize;
        }
        if (this.value < startRange){
            this.value = startRange;
        }
        PXN8.slide.refresh_slider(slider,this.value,startRange,rangeSize);
    };
    if (typeof optionalCallback == 'function'){
        PXN8.event.addListener(slideElement,"mouseup",optionalCallback);
        PXN8.event.addListener(inputElement,"change",optionalCallback);
    }
};

PXN8.slide.refresh_slider = function(slider,value,startRange,rangeSize)
{
    slider.style.left = (3 + (((value-startRange) / rangeSize) * 117)) + "px";
};



/**
 * This method is called when the user mousedowns on a div of class 'pxn8_slide'
 * Every div of class pxn8_slide should have a child div of class 'pxn8_slider'
 * The slide is the horizontal area through which the slider moves. The slider is the
 * bar indicator which indicates where the current position is in the slide.
 * 
 * |--------------------| slide
 *                 ^      slider
 * 
 * -- param slide The slide div
 * -- param event The mouse event which triggered this call (need to obtain position)
 * -- param inputId An input element whose value must be updated whenever the slider is moved
 * -- param start The start value (lowest possible value that can appear in the input element
 *          (basically the lowest in the range)
 * -- param size The range of values that can appear in the input element.
 */
PXN8.slide.onmousedown = function(slide,event,inputId,start,size,increment)
{
    var kids = slide.getElementsByTagName("*");
    var slider = undefined;
    for (var i = 0; i < kids.length; i++){
        if (kids[i].className == "pxn8_slider"){
            slider = kids[i];
            break;
        }
    }
    slider.onmousemove = null;
    var inputElement = document.getElementById(inputId);
    
    slide.onmousemove = function(evt){ 
        return PXN8.slide.update(slider,inputElement,slide,evt,start,size,increment);
    };
    slide.onmouseup = function(){ 
        slide.onmousemove = null; 
    };
    
    PXN8.slide.update(slider,inputElement,slide,event,start,size,increment);    
};
/**
 *
 */
PXN8.slide.update = function(slider,inputElement,slide, evt,start,size,increment)
{ 
    evt = (evt)?evt:window.event;
    var px = PXN8.slide.position(slide);
    var nx = evt.clientX - px;
    if (nx <= 120 && nx >= 3){
        //slider.style.left = (nx-3) + "px";
        var iv = start + (((nx-3) / 117 ) * size);

        // to the nearest increment
        iv = iv - (iv % increment);
        PXN8.slide.refresh_slider(slider,iv,start,size);
        inputElement.value = Math.round(iv,2);
    }
};
/**
 * get the X position of an element relative to it's parent
 */
PXN8.slide.position = function (obj)
{
    var curleft = 0;
    if (obj.offsetParent)
    {
        while (obj.offsetParent)
        {
            curleft += obj.offsetLeft;
            obj = obj.offsetParent;
        }
    }else if (obj.x){
        curleft += obj.x;
    }
    
    return curleft;
};
/* ============================================================================
 *
 * (c) Copyright SXOOP Technologies Ltd. 2005-2007
 * All rights reserved.
 *
 * This file contains code which handles color selection
 *
 */
var PXN8 = PXN8 || {};

PXN8.colors = {};

PXN8.colors.values = ["#000000","#000033","#000066","#000099","#0000CC","#0000FF","#330000","#330033","#330066","#330099","#3300CC",
                      "#3300FF","#660000","#660033","#660066","#660099","#6600CC","#6600FF","#990000","#990033","#990066","#990099",
                      "#9900CC","#9900FF","#CC0000","#CC0033","#CC0066","#CC0099","#CC00CC","#CC00FF","#FF0000","#FF0033","#FF0066",
                      "#FF0099","#FF00CC","#FF00FF","#003300","#003333","#003366","#003399","#0033CC","#0033FF","#333300","#333333",
                      "#333366","#333399","#3333CC","#3333FF","#663300","#663333","#663366","#663399","#6633CC","#6633FF","#993300",
                      "#993333","#993366","#993399","#9933CC","#9933FF","#CC3300","#CC3333","#CC3366","#CC3399","#CC33CC","#CC33FF",
                      "#FF3300","#FF3333","#FF3366","#FF3399","#FF33CC","#FF33FF","#006600","#006633","#006666","#006699","#0066CC",
                      "#0066FF","#336600","#336633","#336666","#336699","#3366CC","#3366FF","#666600","#666633","#666666","#666699",
                      "#6666CC","#6666FF","#996600","#996633","#996666","#996699","#9966CC","#9966FF","#CC6600","#CC6633","#CC6666",
                      "#CC6699","#CC66CC","#CC66FF","#FF6600","#FF6633","#FF6666","#FF6699","#FF66CC","#FF66FF","#009900","#009933",
                      "#009966","#009999","#0099CC","#0099FF","#339900","#339933","#339966","#339999","#3399CC","#3399FF","#669900",
                      "#669933","#669966","#669999","#6699CC","#6699FF","#999900","#999933","#999966","#999999","#9999CC","#9999FF",
                      "#CC9900","#CC9933","#CC9966","#CC9999","#CC99CC","#CC99FF","#FF9900","#FF9933","#FF9966","#FF9999","#FF99CC",
                      "#FF99FF","#00CC00","#00CC33","#00CC66","#00CC99","#00CCCC","#00CCFF","#33CC00","#33CC33","#33CC66","#33CC99",
                      "#33CCCC","#33CCFF","#66CC00","#66CC33","#66CC66","#66CC99","#66CCCC","#66CCFF","#99CC00","#99CC33","#99CC66",
                      "#99CC99","#99CCCC","#99CCFF","#CCCC00","#CCCC33","#CCCC66","#CCCC99","#CCCCCC","#CCCCFF","#FFCC00","#FFCC33",
                      "#FFCC66","#FFCC99","#FFCCCC","#FFCCFF","#00FF00","#00FF33","#00FF66","#00FF99","#00FFCC","#00FFFF","#33FF00",
                      "#33FF33","#33FF66","#33FF99","#33FFCC","#33FFFF","#66FF00","#66FF33","#66FF66","#66FF99","#66FFCC","#66FFFF",
                      "#99FF00","#99FF33","#99FF66","#99FF99","#99FFCC","#99FFFF","#CCFF00","#CCFF33","#CCFF66","#CCFF99","#CCFFCC",
                      "#CCFFFF","#FFFF00","#FFFF33","#FFFF66","#FFFF99","#FFFFCC","#FFFFFF"];

/*
 * Return the HTML to show a color picker
 */
PXN8.colors.picker = function(initColor,callback)
{
    var _ = PXN8.dom;
    
    var closure = function(color,func){
        return function(){ func(color); };
    };
    var table = _.ce("table", {className: "color_table"});
    table.setAttribute("cellpadding","0");
    table.setAttribute("cellspacing","0");
    table.cellPadding = 0;
    table.cellSpacing = 0; // IE
    var cols = 18;
    var rows = Math.ceil(PXN8.colors.values.length/cols);

    var tbody = _.ac(table,_.ce("tbody"));
    for (var i = 0;i < rows;i++)
    {
        var row = _.ac(tbody,_.ce("tr"));
        for (var j = 0; j < cols; j++){
            var cell = _.ac(row,_.ce("td"));
            var index = (i*cols)+j;
            if (index < PXN8.colors.values.length){
                var color = PXN8.colors.values[index];
                var link = _.ce("a");
                link.href="#";
                link.title = color;
                link.onclick = closure(color,callback);
                link.onmouseover = closure(color,function(color){
                    var color_well = _.id("color_well");
                    color_well.style.backgroundColor=color;
                    var color_value = _.id("color_value");
                    color_value.innerHTML = color;
                });
                _.ac(link,_.tx("  "));
                link.style.backgroundColor = color;
                _.ac(cell,link);
            }
        }
    }
    var row = _.ac(tbody,_.ce("tr"));
    var cell1 = _.ac(row,_.ce("td"));
    cell1.colSpan = cols/2;
    cell1.id = "color_well";
    cell1.innerHTML = "";
    cell1.style.backgroundColor = initColor;
    var cell2 = _.ac(row,_.ce("td"));
    cell2.colSpan = cols/2;
    cell2.id = "color_value";
    cell2.innerHTML = initColor;
    return table;
};

/* ============================================================================
 *
 * (c) Copyright SXOOP Technologies Ltd. 2005-2007
 * All rights reserved.
 *
 * This file contains code which handles AJAX / JSON requests
 *
 */

var PXN8 = PXN8 || {};
PXN8.ajax = {};
/***************************************************************************

PXN8.ajax.createRequest
=======================
Create a XMLHttpRequest Object.

Returns
-------
A new XMLHttpRequest Object.

***/
PXN8.ajax.createRequest = function(){

	if (typeof XMLHttpRequest != 'undefined') {
   	 return new XMLHttpRequest();
   }	
   try 	{
       return new ActiveXObject("Msxml2.XMLHTTP");
   } catch (e) {
       try {
           return new ActiveXObject("Microsoft.XMLHTTP");
       } catch (e) { }
   }
   return false;
};
    
/***************************************************************************

PXN8.ajax.submitScript
======================
Submit a series of image-manipulation commands to the server. This is the end-point 
through which all image manipulation commands are passed to the server.

Parameters
----------
* script : An array containing a series of operations to be performed on the photo.
* callback  
A function which will be called when the server has completed the supplied series of operations and returns with *JSON* response.
The callback function should accept a single parameter of type object. 
The object supplied to the callback will have the following important properties...
    * status : A string value that can be eiterh "OK" or "ERROR" (if an error occurred while the server was processing the script.
    * errorMessage : A string value. Blank if no error occurred.
    * image : A relative (to PXN8.root) path to the compressed (bandwidth-friendly) image. This will be empty if an error has occurred.
    * uncompressed : A relative (to PXN8.root) path to the uncompressed (100% quality) image. This will be empty if an error has occurred.

Example
-------

    var script = PXN8.getScript();
    PXN8.ajax.submitScript(script,function(jsonResponse){
        if (jsonResponse.status == "OK"){
           var image = jsonResponse.image;
           // do something with the image
        }else{
           alert(jsonResponse.errorMessage);
        }
    });
***/
PXN8.ajax.submitScript = function(script, callback)
{
    // wph 20070226: 
    // optimize the script.
    //
    // [1] if there are 2 or more sequential resizes, then only use the last resize.
    // [2] if the user is rotating (without flipping ) then aggregate each rotation into 
    // a single operation (modulus 360).
    //
    script = PXN8.optimizeScript(script);
    
    var scriptToText = PXN8.objectToString(script);
    
    var cachedJSON = PXN8.json.scriptCache[scriptToText];
    
    if (cachedJSON){
        //
        // call immediately without going to the server
        //
        callback(cachedJSON);
        return;
    }

    //     
    // ======================================================================
        
    var req = PXN8.ajax.createRequest();
    
    var onJSONerror = function(r){
        alert(unescape(PXN8.strings.WEB_SERVER_ERROR) + "\n" + r.statusText + "\n" + r.responseText) ;
        var timer = document.getElementById("pxn8_timer");
        if (timer){
            timer.style.display = "none";
        }
        PXN8.updating = false;
    };
    
    //PXN8.json.bind(req,callback,onJSONerror);
    // wph 20070131 : use the caching binder instead
    //
    PXN8.json.bindScriptToResponse(req,callback,scriptToText,onJSONerror);
    
    req.open("POST", PXN8.root + "/pxn8.pl", true);
    req.setRequestHeader('Content-Type', 
                         'application/x-www-form-urlencoded');
 
    var submission = "script=" + scriptToText;
    
    req.send(submission);

    //
    
};

/**
 * Perform a series of optimizations on the script
 */
PXN8.optimizeScript = function(script)
{
    var self = PXN8;
    
    for (var i = 0; i < self.optimizations.length; i++){
        var optimize = self.optimizations[i];
        script = optimize(script);
    }
    return script;
};

PXN8.optimizations = [ function(script){
    /**
     * flatten resize ops
     */
    var result = [];
    
    for (var i = 0;i < script.length; i++){
        var op = script[i];
        var nextop = false;
        if (i+1 < script.length){
            nextop = script[i+1];
        }
        if (nextop && op.operation == 'resize' && nextop.operation == 'resize'){
            // 
            // do nothing - skip this operation
            //
        }else{
            result.push(op);
        }
    }
    return result;
},
function(script)
{
    /**
     * remove 2nd + more consecutive normalize operations.
     * (normalize - unlike enhance is not progressive)
     */
    var result = [];
    for (var i =0; i < script.length;i++){
        var op = script[i];
        var nextop = false;
        if (i+1 < script.length){
            nextop = script[i+1];
        }
        if (nextop && (nextop.operation == 'normalize') && (op.operation == 'normalize')){
        }else{
            result.push(op);
        }
    }
    return result;
},
function(script)
{
    var result = [];

    var colorsOp = null;
    //
    // optimizations for consecutive 'colors' operations.
    //
    for (var i = 0; i < script.length; i++){
        var op = script[i];
        if (op.operation != "colors"){
            if (colorsOp != null){
                result.push(colorsOp);
            }
            result.push(op);
            colorsOp = null;
        }else{
            if (colorsOp != null){
                //
                // saturation, brightness and hue are multiplicative
                //
                colorsOp.saturation = ((colorsOp.saturation / 100) * (op.saturation /100)) * 100;
                colorsOp.brightness = ((colorsOp.brightness / 100) * (op.brightness /100)) * 100;
                colorsOp.hue = ((colorsOp.hue / 100) * (op.hue /100)) * 100;
                //
                // contrast is additive
                //
                colorsOp.contrast = colorsOp.contrast  + op.contrast ;
            }else{
                colorsOp = op;
            }
        }
    }
    if (colorsOp != null){
        result.push(colorsOp);
    }

    return result;
    
},

function(script){
    /**
     * modulus 360 all consecutive rotate ops
     */
    var result = [];
    for (var i = 0;i < script.length; i++){
        var op = script[i];
        var nextop = false;
        if (i+1 < script.length){
            nextop = script[i+1];
        }
        if (nextop && (nextop.operation == 'rotate') && (op.operation == 'rotate')){
            //
            //
            //
            var flipping = (op.flipvt || op.fliphz || nextop.flipvt || nextop.fliphz);
            if (!flipping) {
                nextop.angle = (op.angle + nextop.angle) % 360;
            }else{
                //
                // it's a flip
                // is it the same type of flip as next op and are angles 0 in both cases ?
                if ((op.angle == 0 && nextop.angle == 0) && ((op.flipvt == nextop.flipvt) && (op.fliphz == nextop.fliphz))){
                    //
                    // it's two flipvts in a row or two fliphzs in a row
                    //
                    i += 1;
                }else{
                    result.push(op);
                }
                
            }
        }else{
            if (op.operation == 'rotate'){
                var flipping = (op.flipvt || op.fliphz || nextop.flipvt || nextop.fliphz);
                if (!flipping && op.angle == 0){
                    // skip operation - it's effectively a NOP
                }else{
                    // it's a straight rotation with an angle > 0
                    result.push(op);
                }
            }else{
                result.push(op);
            }
        }
    }
    return result;
}];

PXN8.json = {};

PXN8.json.bind = function(request,callback,onerror)
{
    request.onreadystatechange = function(){
        if (request.readyState == 4) {
            
            if (request.status == 200) {
                var json ;
                
                try{
                    var jsonText = request.responseText;
                    json  = eval('('+ jsonText + ')');
                }catch (e){
                    alert("An exception occured tring to evaluate server response:\n" +
                          request.responseText);
                }
                callback(json);
            } else {
                if (onerror){
                    onerror(request);
                }else{
                    alert(unescape(PXN8.strings.WEB_SERVER_ERROR) + "\n" + request.statusText + "\n" + request.responseText) ;
                }
            }
        }
    };
};
/**
 * wph 20070131
 * Store scriptText/responseText pairings in a cache
 * avoid unnecessary calls to the server
 */
PXN8.json.bindScriptToResponse = function(request,callback,scriptAsString,onerror)
{
    request.onreadystatechange = function(){
        if (request.readyState == 4) {
            
            if (request.status == 200) {
                var json ;
                try{
                    json  = eval('('+ request.responseText + ')');
                    //
                    // store request/response pairing in the cache
                    //
                    PXN8.json.scriptCache[scriptAsString] = json;
                }catch (e){
                    alert("An exception occured tring to evaluate server response:\n" +
                          request.responseText + "\nException: " + e);
                }
                callback(json);
            } else {
                if (onerror){
                    onerror(request);
                }else{
                    alert(unescape(PXN8.strings.WEB_SERVER_ERROR) + "\n" + request.statusText + "\n" + request.responseText) ;
                }
            }
        }
    };
};
/**
 * An associative array of scriptText / responseText pairings.
 */
PXN8.json.scriptCache = [];

    /* ============================================================================
 *
 * (c) Copyright SXOOP Technologies Ltd. 2005-2007
 * All rights reserved.
 *
 * This file contains code which handles saving of images
 *
 */
var PXN8 = PXN8 || {};

/*************************************************************************

SECTION: Saving edited Photos
=============================
The following functions are used for saving the edited photo to the user's
client-side storage or to the server's own storage on the web.

***/

PXN8.save = {};

/**************************************************************************

PXN8.save.toDisk()
==================
Save the photo to the user's client-side storage.

***/
PXN8.save.toDisk = function()
{
    var uncompressedImage = PXN8.getUncompressedImage();
    if (uncompressedImage){
        var newURL = PXN8.server + PXN8.root + "/save.pl?";
        
        if (typeof pxn8_original_filename == "string"){
            newURL += "originalFilename=" + pxn8_original_filename + "&";
        }
        
        newURL += "image=" + uncompressedImage;
        
        document.location = newURL;
            
    }else{
        document.location = "#";
        PXN8.show.alert("You have not changed the image !");
    }
};

/**************************************************************************

PXN8.save.toServer()
====================
Save to server is a wrapper function. It in turn will call *pxn8_save_image()* 
which is a function which must be implemented by the customer.

***/
PXN8.save.toServer = function()
{

    var relativeFilePathToUncompressedImage = PXN8.getUncompressedImage();

    /**
     * wph 20070102 : Don't prohibit the user from saving just because they haven't changed
     * the image.
     * Let the custom pxn8_save_image() function handle that case if needed.
     *
     if (!relativeFilePathToUncompressedImage){
     alert("The image has not been modified.");
     return false;
     }
    */

    if (typeof pxn8_save_image == 'function'){
        return pxn8_save_image(relativeFilePathToUncompressedImage);
    } else {
    
        alert("This feature is not available by default.\n" +
              "To enable this feature you must create a PHP,ASP or JSP page to save the image to your own server.\n" +
              "You must also create a javascript function called 'pxn8_save_image()' - it's first parameter is the URL of the changed image.\n" +
              "The path to the changed image (relative to the directory where PXN8 is installed) is " + PXN8.getUncompressedImage());
        return false;
    }
    
};

/**************************************************************************

pxn8_save_image()
=================
This function is not provided by Pixenate but must be implemented by the customer
if you want to be able to save edited photos to your own webserver.

Parameters
----------

* imagePath : A path to the image which should be saved.

The imagePath parameter will be a path relative to the directory where pixenate is installed.

For example the imagePath parameter might be *cache/03_04fbcedaf099feded02working.jpg*.
If Pixenate is installed at /var/www/html/pixenate then the actual path to the file will be...

    /var/www/html/pixenate/cache/03_04fbcedaf099feded02working.jpg

... so to save the image to your webserver's filesystem or database you need to copy the image at the 
above path to your own permanent storage.
You should provide a .PHP, .JSP, .ASP or CGI program to do just this.
Your pxn8_save_image() function should call this CGI passing the *imagePath* value as a parameter
to the server program.

***/

// 