
 
    /*
    http://www.JSON.org/json2.js
    2009-04-16

    Public Domain.

    NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.

    See http://www.JSON.org/js.html

    This file creates a global JSON object containing two methods: stringify
    and parse.

        JSON.stringify(value, replacer, space)
            value       any JavaScript value, usually an object or array.

            replacer    an optional parameter that determines how object
                        values are stringified for objects. It can be a
                        function or an array of strings.

            space       an optional parameter that specifies the indentation
                        of nested structures. If it is omitted, the text will
                        be packed without extra whitespace. If it is a number,
                        it will specify the number of spaces to indent at each
                        level. If it is a string (such as '\t' or '&nbsp;'),
                        it contains the characters used to indent at each level.

            This method produces a JSON text from a JavaScript value.

            When an object value is found, if the object contains a toJSON
            method, its toJSON method will be called and the result will be
            stringified. A toJSON method does not serialize: it returns the
            value represented by the name/value pair that should be serialized,
            or undefined if nothing should be serialized. The toJSON method
            will be passed the key associated with the value, and this will be
            bound to the object holding the key.

            For example, this would serialize Dates as ISO strings.

                Date.prototype.toJSON = function (key) {
                    function f(n) {
                        // Format integers to have at least two digits.
                        return n < 10 ? '0' + n : n;
                    }

                    return this.getUTCFullYear()   + '-' +
                         f(this.getUTCMonth() + 1) + '-' +
                         f(this.getUTCDate())      + 'T' +
                         f(this.getUTCHours())     + ':' +
                         f(this.getUTCMinutes())   + ':' +
                         f(this.getUTCSeconds())   + 'Z';
                };

            You can provide an optional replacer method. It will be passed the
            key and value of each member, with this bound to the containing
            object. The value that is returned from your method will be
            serialized. If your method returns undefined, then the member will
            be excluded from the serialization.

            If the replacer parameter is an array of strings, then it will be
            used to select the members to be serialized. It filters the results
            such that only members with keys listed in the replacer array are
            stringified.

            Values that do not have JSON representations, such as undefined or
            functions, will not be serialized. Such values in objects will be
            dropped; in arrays they will be replaced with null. You can use
            a replacer function to replace those with JSON values.
            JSON.stringify(undefined) returns undefined.

            The optional space parameter produces a stringification of the
            value that is filled with line breaks and indentation to make it
            easier to read.

            If the space parameter is a non-empty string, then that string will
            be used for indentation. If the space parameter is a number, then
            the indentation will be that many spaces.

            Example:

            text = JSON.stringify(['e', {pluribus: 'unum'}]);
            // text is '["e",{"pluribus":"unum"}]'


            text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
            // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'

            text = JSON.stringify([new Date()], function (key, value) {
                return this[key] instanceof Date ?
                    'Date(' + this[key] + ')' : value;
            });
            // text is '["Date(---current time---)"]'


        JSON.parse(text, reviver)
            This method parses a JSON text to produce an object or array.
            It can throw a SyntaxError exception.

            The optional reviver parameter is a function that can filter and
            transform the results. It receives each of the keys and values,
            and its return value is used instead of the original value.
            If it returns what it received, then the structure is not modified.
            If it returns undefined then the member is deleted.

            Example:

            // Parse the text. Values that look like ISO date strings will
            // be converted to Date objects.

            myData = JSON.parse(text, function (key, value) {
                var a;
                if (typeof value === 'string') {
                    a =
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
                    if (a) {
                        return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
                            +a[5], +a[6]));
                    }
                }
                return value;
            });

            myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
                var d;
                if (typeof value === 'string' &&
                        value.slice(0, 5) === 'Date(' &&
                        value.slice(-1) === ')') {
                    d = new Date(value.slice(5, -1));
                    if (d) {
                        return d;
                    }
                }
                return value;
            });


    This is a reference implementation. You are free to copy, modify, or
    redistribute.

    This code should be minified before deployment.
    See http://javascript.crockford.com/jsmin.html

    USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
    NOT CONTROL.
*/

/*jslint evil: true */

/*global JSON */

/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
    call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
    getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
    lastIndex, length, parse, prototype, push, replace, slice, stringify,
    test, toJSON, toString, valueOf
*/

// Create a JSON object only if one does not already exist. We create the
// methods in a closure to avoid creating global variables.

if (!this.JSON) {
    JSON = {};
}
(function () {

    function f(n) {
        // Format integers to have at least two digits.
        return n < 10 ? '0' + n : n;
    }

    if (typeof Date.prototype.toJSON !== 'function') {

        Date.prototype.toJSON = function (key) {

            return this.getUTCFullYear()   + '-' +
                 f(this.getUTCMonth() + 1) + '-' +
                 f(this.getUTCDate())      + 'T' +
                 f(this.getUTCHours())     + ':' +
                 f(this.getUTCMinutes())   + ':' +
                 f(this.getUTCSeconds())   + 'Z';
        };

        String.prototype.toJSON =
        Number.prototype.toJSON =
        Boolean.prototype.toJSON = function (key) {
            return this.valueOf();
        };
    }

    var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
        escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
        gap,
        indent,
        meta = {    // table of character substitutions
            '\b': '\\b',
            '\t': '\\t',
            '\n': '\\n',
            '\f': '\\f',
            '\r': '\\r',
            '"' : '\\"',
            '\\': '\\\\'
        },
        rep;


    function quote(string) {


// If the string contains no control characters, no quote characters, and no
// backslash characters, then we can safely slap some quotes around it.
// Otherwise we must also replace the offending characters with safe escape
// sequences.

        escapable.lastIndex = 0;
        return escapable.test(string) ?
            '"' + string.replace(escapable, function (a) {
                var c = meta[a];
                return typeof c === 'string' ? c :
                    '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
            }) + '"' :
            '"' + string + '"';
    }


    function str(key, holder) {

// Produce a string from holder[key].

        var i,          // The loop counter.
            k,          // The member key.
            v,          // The member value.
            length,
            mind = gap,
            partial,
            value = holder[key];

// If the value has a toJSON method, call it to obtain a replacement value.

        if (value && typeof value === 'object' &&
                typeof value.toJSON === 'function') {
            value = value.toJSON(key);
        }

// If we were called with a replacer function, then call the replacer to
// obtain a replacement value.

        if (typeof rep === 'function') {
            value = rep.call(holder, key, value);
        }

// What happens next depends on the value's type.

        switch (typeof value) {
        case 'string':
            return quote(value);

        case 'number':

// JSON numbers must be finite. Encode non-finite numbers as null.

            return isFinite(value) ? String(value) : 'null';

        case 'boolean':
        case 'null':

// If the value is a boolean or null, convert it to a string. Note:
// typeof null does not produce 'null'. The case is included here in
// the remote chance that this gets fixed someday.

            return String(value);

// If the type is 'object', we might be dealing with an object or an array or
// null.

        case 'object':

// Due to a specification blunder in ECMAScript, typeof null is 'object',
// so watch out for that case.

            if (!value) {
                return 'null';
            }

// Make an array to hold the partial results of stringifying this object value.

            gap += indent;
            partial = [];

// Is the value an array?

            if (Object.prototype.toString.apply(value) === '[object Array]') {

// The value is an array. Stringify every element. Use null as a placeholder
// for non-JSON values.

                length = value.length;
                for (i = 0; i < length; i += 1) {
                    partial[i] = str(i, value) || 'null';
                }

// Join all of the elements together, separated with commas, and wrap them in
// brackets.

                v = partial.length === 0 ? '[]' :
                    gap ? '[\n' + gap +
                            partial.join(',\n' + gap) + '\n' +
                                mind + ']' :
                          '[' + partial.join(',') + ']';
                gap = mind;
                return v;
            }

// If the replacer is an array, use it to select the members to be stringified.

            if (rep && typeof rep === 'object') {
                length = rep.length;
                for (i = 0; i < length; i += 1) {
                    k = rep[i];
                    if (typeof k === 'string') {
                        v = str(k, value);
                        if (v) {
                            partial.push(quote(k) + (gap ? ': ' : ':') + v);
                        }
                    }
                }
            } else {

// Otherwise, iterate through all of the keys in the object.

                for (k in value) {
                    if (Object.hasOwnProperty.call(value, k)) {
                        v = str(k, value);
                        if (v) {
                            partial.push(quote(k) + (gap ? ': ' : ':') + v);
                        }
                    }
                }
            }

// Join all of the member texts together, separated with commas,
// and wrap them in braces.

            v = partial.length === 0 ? '{}' :
                gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' +
                        mind + '}' : '{' + partial.join(',') + '}';
            gap = mind;
            return v;
        }
    }

// If the JSON object does not yet have a stringify method, give it one.

    if (typeof JSON.stringify !== 'function') {
        JSON.stringify = function (value, replacer, space) {

// The stringify method takes a value and an optional replacer, and an optional
// space parameter, and returns a JSON text. The replacer can be a function
// that can replace values, or an array of strings that will select the keys.
// A default replacer method can be provided. Use of the space parameter can
// produce text that is more easily readable.

            var i;
            gap = '';
            indent = '';

// If the space parameter is a number, make an indent string containing that
// many spaces.

            if (typeof space === 'number') {
                for (i = 0; i < space; i += 1) {
                    indent += ' ';
                }

// If the space parameter is a string, it will be used as the indent string.

            } else if (typeof space === 'string') {
                indent = space;
            }

// If there is a replacer, it must be a function or an array.
// Otherwise, throw an error.

            rep = replacer;
            if (replacer && typeof replacer !== 'function' &&
                    (typeof replacer !== 'object' ||
                     typeof replacer.length !== 'number')) {
                throw new Error('JSON.stringify');
            }

// Make a fake root object containing our value under the key of ''.
// Return the result of stringifying the value.

            return str('', {'': value});
        };
    }


// If the JSON object does not yet have a parse method, give it one.

    if (typeof JSON.parse !== 'function') {
        JSON.parse = function (text, reviver) {

// The parse method takes a text and an optional reviver function, and returns
// a JavaScript value if the text is a valid JSON text.

            var j;

            function walk(holder, key) {

// The walk method is used to recursively walk the resulting structure so
// that modifications can be made.

                var k, v, value = holder[key];
                if (value && typeof value === 'object') {
                    for (k in value) {
                        if (Object.hasOwnProperty.call(value, k)) {
                            v = walk(value, k);
                            if (v !== undefined) {
                                value[k] = v;
                            } else {
                                delete value[k];
                            }
                        }
                    }
                }
                return reviver.call(holder, key, value);
            }


// Parsing happens in four stages. In the first stage, we replace certain
// Unicode characters with escape sequences. JavaScript handles many characters
// incorrectly, either silently deleting them, or treating them as line endings.

            cx.lastIndex = 0;
            if (cx.test(text)) {
                text = text.replace(cx, function (a) {
                    return '\\u' +
                        ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
                });
            }

// In the second stage, we run the text against regular expressions that look
// for non-JSON patterns. We are especially concerned with '()' and 'new'
// because they can cause invocation, and '=' because it can cause mutation.
// But just to be safe, we want to reject all unexpected forms.

// We split the second stage into 4 regexp operations in order to work around
// crippling inefficiencies in IE's and Safari's regexp engines. First we
// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
// replace all simple value tokens with ']' characters. Third, we delete all
// open brackets that follow a colon or comma or that begin the text. Finally,
// we look to see that the remaining characters are only whitespace or ']' or
// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.

            if (/^[\],:{}\s]*$/.
test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@').
replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').
replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {

// In the third stage we use the eval function to compile the text into a
// JavaScript structure. The '{' operator is subject to a syntactic ambiguity
// in JavaScript: it can begin a block or an object literal. We wrap the text
// in parens to eliminate the ambiguity.

                j = eval('(' + text + ')');

// In the optional fourth stage, we recursively walk the new structure, passing
// each name/value pair to a reviver function for possible transformation.

                return typeof reviver === 'function' ?
                    walk({'': j}, '') : j;
            }

// If the text is not JSON parseable, then a SyntaxError is thrown.

            throw new SyntaxError('JSON.parse');
        };
    }
}());


    /***  PLUtil stuff **/
/** Return true if it is a String */
plIsString = function(/*anything*/ it){
    return (typeof it == "string" || it instanceof String); // Boolean
}

/** Return true if it is an Array.
//		Does not work on Arrays created in other windows.
*/
plIsArray = function(/*anything*/ it){
    return it && (it instanceof Array || typeof it == "array"); // Boolean
}

/** Return true if it is a Function */
plIsFunction = function(/*anything*/ it){
    var opts = Object.prototype.toString;
    return opts.call(it) === "[object Function]";
};


/** Returns true if it is a JavaScript object (or an Array, a Function or null
 **/
plIsObject = function(/*anything*/ it){
        return it !== undefined &&
                (it === null || typeof it == "object" || plIsArray(it) || plIsFunction(it)); // Boolean
}

/** slow version from dojo */
plToArray = function(obj, offset, startWith)
{
        var arr = startWith||[];
        for(var x = offset || 0; x < obj.length; x++){
                arr.push(obj[x]);
        }
        return arr;
};

/**
 * Write a log message to a console window. if window.console is not defined, then no action will be performed.
 * @param {String} message String to display or log
 * @level {String} level Level of the log message. Optional, Only 'warn' is supported.
 * */
function plDebug (message, level)
{

	if ( typeof(window.console) != "undefined") // does console exist?
	{
		if (level == "warn" && typeof(window.console.warn) != undefined ) window.console.warn (message); // presuming IE and FF both have console.warn?(rough hack GK 20090722)
		else
		{
			if ( window.console.debug ) window.console.debug(message);// this is probably FF (rough hack GK 20090722) 
			else if ( window.console.log ) window.console.log (message);
			//else alert ( "debug messsage cannot be written to the console. The message is: " + message);
		}
	 } 
	 //else alert ( "debug messsage cannot be written to the console (2). The message is: " + message);
	 
}

/** Gets the "head" element in the HTML. If a head is not defined, then returns the document object.
 *Useful for injecting script tags dynamically
 *@return Head node or document node
 *@type HTMLElement
 */
    function plGetHead()
    {
        var nl = document.getElementsByTagName('head');
        if ( nl && nl.length > 0 && nl.item(0)) return nl.item(0);
        else return document;
    }

/** Loads a script dynalically
 *@param {string] url URL for the script source.
 **@param {object} callback array with onsuccess, onfailure,..
 */

if ( typeof plLoadScript == "undefined" || !plLoadScript ) {  //if plLoadScript is not defined.  Typically this should be pre/overriden by a better implementation
    function plLoadScript (url, callback )
    {
        plDebug("loading script " + url);
        var script = document.createElement('script');
        script.src = url;
        script.type = 'text/javascript';
        var id = plGetNewCallId();
        script.id = id;
        var head = plGetHead();
        head.appendChild(script);

        if ( typeof callback != "undefined" && callback && callback.onsuccess)
        {
            var done = false;
	        callback["id"] = id;
            // Attach handlers for all browsers
            script.onload = script.onreadystatechange = function()
              {
                if ( !done && (!this.readyState || this.readyState === "loaded" || this.readyState === "complete") )
                {
                    done = true;
                    var data = callback.id;
                    if ( callback.onsuccess ) callback.onsuccess.call(callback.context||window, data, "success"); //TODO handle Scope
                    //if ( callback.oncomplete ) callback.onComplete.call(); //TODO handle Scope
                    // Handle memory leak in IE
                    script.onload = script.onreadystatechange = null;
                    if ( head && script.parentNode ) {
                        //head.removeChild( script );
                    }
                }
            };
        }
		
    }
}

/** Loads Style sheets dynamically
 *@param {String} url URL of the Style sheet
 *@param {object} callback array with onsuccess, onfailure,..
 **/
if ( typeof plLoadStyles == "undefined" || ! plLoadStyles ){
	function plLoadStyles(url, callback)
    {
        plDebug("loading styles " + url);
        var s = document.createElement('link');
        s["href"] = url;
        s["type"] = 'text/css';
        s["rel"]  = "stylesheet";
        var head  = plGetHead();
        head.appendChild(s);
	}
}

        pl_transport = "script"; //The transport to use to make requests to PlaceLogic. Supported transports are script,ajax,flash
                                // script: adds a script node
                                // ajax : uses Yahoo Connect (XmlHttpRequest object)
                                // ajaxproxy : uses Yahho Connect ajax to a proxy service
                                // flash : Uses Yahoo Connect XDR (using flash)


   /**Convenience method to call PlaceLogic REST APIs using the defined transport in pl_transport.
    * Can be used to access data across domains.
    * For "ajax" transport, the placelogic.jsp must be placed on the customer web server and Yahoo.util.Connect scripts must be loaded
    * For "script" transport, there are no dependencies
    * "flash" transport is not implemented.
    *@param {int} callid A unique id for the service call
    *@param {String} url URL of the REST service
    *@param {Object} Object literal containing properties that would be available for the client side callback function. These properties
    *@param {String} transportString Transport to use. "script", "ajax", "ajaxproxy" or "flash". If not provided, the pl_transport value would be used.
    * will not be posted to the server.
    * @see pl_transport
    * @see Yahoo.util.Connect.
    **/

if ( typeof plCallXDomain == "undefined" || !plCallXDomain ) {
    function plCallXDomain (callid, url, clientSideProps, transportString)
    {
        plSetCallProperties(callid, clientSideProps);
        var transport = transportString ? transportString : pl_transport;
        plDebug( 'transport=' + transport + ' callid=' + callid + ' url=' + url);
        if ( "ajax" == transport || "flash" ==  transport || "ajaxproxy" == transport )
        {
            var qsStart = url.indexOf("?");
            var callback = {success: plAjaxResponseCallback, failure: plAjaxResponseCallback, argument:{callid: callid, callbackfunction:"", mode: ""}};

            //break the url, to call the proxy and post the query string data
            //also manipulate the callback mechanism
            //var url = "http://localhost:8084/placelogic/services/placelist/json/list.action?fsid=3&callback=cb"
            //plDebug("qsStart=" + qsStart);
            var qs = "";
            var urlpart = "";
            var service = "";
            var action="";
            var mode="json";
            var content="";
            var callbackFuncName = "";
            var serviceUrl = url;
            if ( qsStart > 0 )
            {
                qs = url.substring(qsStart+1);
                urlpart = url.substring(0,qsStart);

                if ( "ajaxproxy" == transport )
                {
                    //url is of the format http://domain:port/placelogic/services/SERVICE/MODE/ACTION.action
                    //strip these out
                    var pattern = "/placelogic/services/";
                    var sstart = urlpart.indexOf(pattern);
                    if ( sstart < 0) throw new Error("Invalid URL for ajax transport.");
                    sstart += pattern.length;
                    urlpart = urlpart.substring(sstart);
                    urlpart = urlpart.trim();
                    urlpart = urlpart.replace(/\.action$/, "");
                    var parts = urlpart.split("/");
                    if ( !parts || parts.length != 3) throw new Error("Invalid URL for PlaceLogic ajax transport.");
                    service = parts[0]; mode = parts[1]; action=parts[2];

                    content = "_service=" + service + "&_mode=" + mode + "&_action=" + action
                            + "&" + qs + "&";
                    //plDebug("content=" + content);
                    serviceUrl = "placelogic.jsp";
                }
                else
                {
                    content = "_ajax=true&" + qs +"&";
                    serviceUrl = urlpart;
                }
                var cbmatches = content.match(/&callback=[a-z0-9\\._%]*&/i);
                for ( var ci = 0; ci < cbmatches.length; ci ++)
                {
                    if ( ! plIsEmpty(cbmatches[ci] ) )
                    {
                        content = content.replace(cbmatches[ci], "&"); //remove the callback from the service request
                        callbackFuncName = cbmatches[ci].replace(/callback=/, "");
                        callbackFuncName = callbackFuncName.replace(/&/g, "");
                        break;
                    }
                }
                //plDebug("content=" + content);
                callback.argument["callbackfunction"]=callbackFuncName;
            }
            if ( url.indexOf("/json/") > 0 ) mode = "json";
            else if ( url.indexOf("/xml/") > 0 ) mode = "xml";
            else if ( url.indexOf("/html/") > 0 ) mode = "html";
            else if ( url.indexOf("/kml/") > 0 ) mode = "kml";
            callback.argument["mode"] = mode;
            if ( "ajax" == transport || "ajaxproxy" == transport )
            {
                if ( YAHOO && YAHOO.util && YAHOO.util.Connect )
                {
                    YAHOO.util.Connect.asyncRequest('POST', serviceUrl, callback, content );
                }
                else throw new Error("Yahoo.util.Connect is not loaded for Ajax requests. TODO, try creating a XmlHtttpRequest object");
            }
            else if ( "flash" == transport )
            {
                throw new Error("TODO: Yahoo Connect xdr");
            }
        }
        else // use a script node transport
        {
            //TODO -- split a request into multiple parts based on url size.
            if ( url.length > 2048 ) plDebug("URL size maybe a problem", "warn");

            var script = document.createElement('script');
            script.setAttribute("id", "plEleCall" + callid);
            script.setAttribute("src",url);
            script.setAttribute("type","text/javascript");
            var head = plGetHead();
            head.appendChild(script);

        }
    }
}
    
    /** Initial callback for Ajax requests. Will invoke the provided final callback
     *@param {Object} o Yahoo Connect Ajax Response object
     *
     **/
    function plAjaxResponseCallback(o)
    {
        plDebug("In plAjaxResponseCallback");
        var args = o.argument;
        var resp;
        if ( "json" == args["mode"])
        {
            //plDebug('evaluating reponse');
            plDebug("parsing json");
            try
            {
                resp = eval("[" + o.responseText + "]")[0]; //need to append into an array to evaluate
            }
            catch (e)
            {
                plDebug("plAjaxResponseCallback, parse json failed, msg=" + e.message);
                throw new Error("Service response failed");
            }
        }
        else
        {
            //plDebug('response as text');
            resp = o.responseText;
        }
        //plDebug(resp);
        var cb = args["callbackfunction"];
        if ( !plIsEmpty(cb))
        {
            plDebug(cb)
            if ( cb.indexOf(".") > 0 ) // the callback is a object reference
            {
                var obj = eval (cb);
                obj.call(window, resp, args["callid"]);
            }
            else //assuming a window reference would do
            {
                window[cb].call(window, resp, args["callid"]);
            }
        }
    }    


    /** Sets the client side Call properties in an internal arra "pl_callprops"
     * @private
     * @param {int} callid A unique id for the service call
     * @param {Object} Object literal containing properties that would be available for the client side callback function.
     */

    function plSetCallProperties(callid, props)
    {
        if ( typeof (pl_callprops) == "undefined")
        {
            pl_callprops = {};
        }
        pl_callprops["call_"+ callid] = props;
    }

    /** Clears the client side Call properties in an internal arra "pl_callprops"
     * @private
     * @param {int} callid A unique id for the service call
     */
    function plClearCallProperties(callid)
    {
        if ( typeof (pl_callprops) != "undefined")
        {
            if ( pl_callprops["call_"+ callid])
                pl_callprops["call_"+ callid] = {}
        }
    }



    /** Gets the client side Call properties in an internal arra "pl_callprops"
     * @private
     * @return Object literal containing properties
     * @type Object
     * @param {int} callid A unique id for the service call
     * @param {Object} Object literal containing properties that would be available for the client side callback function.
     */
    function plGetCallProperties(callid)
    {
        if ( typeof (pl_callprops) != "undefined")
        {
            if ( pl_callprops["call_"+ callid])
                return pl_callprops["call_"+ callid];
            else
                return {};
        }
        else
            return {};
    }

    /** Generates an unique id for each service call
     *@return Id
     *@type int
     **/
  if ( typeof plGetNewCallId == "undefined" || !plGetNewCallId ){
	function plGetNewCallId()
	{
		return Math.floor(Math.random().toPrecision(18)*10e18);
	}
  }

  if ( typeof plCleanupCall == "undefined"  || !plCleanupCall) {
    //cleans up, deletes the contents of a xdomain call (script tag)
    function plCleanupCall(callid)
    {
        plDebug("cleaning up after call, removing xdomain script tag");
        var head = plGetHead();
        var s = plById("plEleCall"+ callid);
        if ( s && head) head.removeChild(s);
    }
  }

    /**Gets a HTML Element object by its id
     *@return HTML Element
     *@type HTMLElement
     *@param {String} id Id of the HTML element
     *@param {boolean} required. Whether the HTML Element is required to exist. Optional.
     *@throws Error 'Element "{id}" is not defined in the HTML if required is set to true
     */
    function plById(id, required)
    {
        var e = document.getElementById(id);
        if (!e || plIsNull(e) || !e.id )
        {
            if ( required && true == required )
                throw new Error("Element " + id + " is not defined in the HTML!");
            else
                plDebug("Element " + id + " is not defined in the HTML!","warn");
        }
        return e;

    }

    /** Convenience method to parse a delimted String containing Name value pairs.
     *@param {String} str Input String
     *@param {String} colDelim} Character(s) to use as the column delimter or the name,value seperator.
     *@param {String} rowDelim Character(s) to use as the row delim, seperating value pairs
     *@returns Object literal containing name value pairs
     *@type Object
     **/
	function plParseSVString(str, colDelim, rowDelim)
	{
	    var attrs = {};
	    if ( plIsEmpty(str)) return attrs;

        var rows = str.split(rowDelim);
        for ( var i = 0; i< rows.length; i++)
        {
            var nv = rows[i];

            if ( !plIsEmpty(nv))
            {
                var nvp = nv.split(colDelim);
                var n = "";
                var v = "";
                if ( nvp.length >= 2)
            {
                n = nvp[0];
                v = nvp[1];
            }
                else if ( nvp.length == 1)
            {
                n = nvp[0];
            }
                n = plCoalesce(n, "");
                n = plTrim(n);
                if ( n == "undefined") n = "";
                if ( !plIsEmpty(n))
                {
                    v = plCoalesce(v, "");
                    if ( v == "undefined") v = "";
                    attrs[n] = v;
                }
            }
        }
        return attrs;
    }

    /**Check if a String is null or undefined
     *@returns returns true if val is null, else false
     *@param {String} val Input value
     *@type boolean
     **/
    function plIsNull (val) //
    {
        var o = val;
        /*
        if ( plIsString(val))
        {
            try
            {
                o = eval(val);
            }
            catch (e)
            {
                plDebug("plIsNull failed while eval " + val + " " + e.message);
                return true;
            }
        }*/
        if ( typeof val == 'undefined'  || undefined == val
                 || 'undefined' == val  || null == val || null === val  )
                 return true;
        else
                 return false;
    }

    /**
     *Checks if given value is empty
     *@returns returns true if val is null or empty, else false.
     **@type boolean
     *@param {String|Array} val Given Value can be a string, array

     **/
    function plIsEmpty(val) //
    {
        if ( plIsNull(val)|| val.length <= 0 ) return true;
        else return false;
    }

    /**Trims leading and trailing spaces from a string
     *@return String with leading and trailing spaces trimed
     *@type String
     *@param {String} str Input String
     */
    function plTrim(str)
    {
        if ( plIsEmpty(str)) return str;
        else return str.replace(/^\s+|\s+$/g, '') ;
    }


    function plCoalesce(val, def)
    {
        try
        {
            return ( plIsEmpty(val)?def : val);
        }
        catch (e)
        {
            //swallow error, return the def value
            return def;
        }
    }

    // a.b.c.d checks if the a.b a.b.c and a.b.c.d is available w/o throwing an error
    /*
    function plIsPropertyAvailable(val)
    {
        if ( ! val) return true;
        var a = val.split(".");
        if ( a && a.length <=1 ) return (val)?true:false;
        else if( a && a.length > 1 )
        {
            var path = a[0];
            for ( var i=1; i< a.length; i++)
            {
                path += "." + a[i];
                var e = eval.call("((" + path + ")?true:false)" );
                plDebug(" eval " + path + ":" + e);
                if ( !e) return false;
            }
            return true;
        }
        else
            return false;
    }

    function plGetProperty(obj, pathStr)
    {
        if ( ! pathStr) return obj;
        var a = pathStr.split(".");
        if ( a && a.length <=1 ) return (obj[a[0]]);
        else if( a && a.length > 1 )
        {
            var path = a[0];
            for ( var i=1; i< a.length; i++)
            {
                path += "." + a[i];
                var e = eval("((" + path + ")?true:false" );
                plDebug(" eval " + path + ":" + e);
                if ( !e) return false;
            }
            return true;
        }
        else
            return false;
    }
    */



    function plIsObjectEmpty (val)
    {
        if ( val == undefined || typeof val != 'object' || val == null || val === null ) return true;
            for ( var k in val ) return false; // there is atleast one member
            return true;
    }

    function plToBoolean(val)
    {
        if ( plIsEmpty(val)) return false;
        var v = (val + "").trim();
        var e = /^1|true|yes|t$/ig;
        if ( v.matches(e) )
        {
            return true;
        }
        else
            return false;
    }

    //Gets a value of a HTMLFormElement
    //param field HTMLFormElement object
    //returns String
	// in the case of radio button, pass the group as array, i.e. results of getElementsByName()
	// TODO all argument to be a field NAME (test typeof == string, plById(field), etc.
    function plGetFormElementValue(field)
    {
        var val = '';
		if ( !plIsObjectEmpty(field))
        {
                switch (field.type)
                {
                    case  'text' :
                    case  'textarea' :
                    case  'hidden' :
                    case  'password' :
                    case  'button' :
                    {
                        val = field.value;
                        break;
                    }
                    case 'select-one' :
                    {
                        val = field.options[field.options.selectedIndex].value;
                        break;
                    }
                    case 'checkbox':
                    {
                        if ( field.checked) val = field.value;
                        else val = null;
                        break;
                    }
                    default :
                    {
						
						if ( field.type == undefined ) // maybe it's a radio button group?
						{
							var value = null;
							for ( var c = 0; c < field.length; c ++ )
							{
								var button = field[c];
								if (button.checked) 
								{
									value = button.value;
									break;
								}
							}
							return value;
						}
						else throw new Error("getFormElementValue: unhandled type " + field.type );
                    }
                }
                /*
                if ( field.type == 'text' || field.type == 'textarea' || field.type=='hidden' || field.type=="password") val = field.value;
                else if ( field.type == 'select-one' ) val = field.options[field.options.selectedIndex].value;
                else if ( field.type == 'checkbox' )
                {
                    for ( var i=0; i<field.options.length; i++)
                    {
                        if ( field.options[i].checked )
                        {
                            val = field.options[i].value;
                            break;
                        }
                    }
                }
                else if ( field.type == 'button' )
                {
                    val
                }
                else throw("getFormElementValue: unhandled type " + field.type);
                 */
        }
        return val;
    }

    //Sets a value of a HTMLFormElement
    //param field HTMLFormElement object
    //returns String
	// in the case of radio button, pass the group as array, i.e. results of getElementsByName()
	// TODO all argument to be a field NAME (test typeof == string, plById(field), etc.
    function plSetFormElementValue(field, val)
    {
        if ( !plIsObjectEmpty(field))
        {
                switch (field.type)
                {
                    case  'text' :
                    case  'textarea' :
                    case  'hidden' :
                    case  'password' :
                    case  'button' :
                    {
                        field.value = val;
                        break;
                    }
                    case 'select-one' :
                    {
                        for ( var i = 0; i < field.options.length; i++ )
                        {
                            field.options[i].selected = false;
                            if ( field.options[i].value == val)
                            {
                                field.options[i].selected = true;
                            }
                        }
                        break;
                    }
                    case 'checkbox':
                    {
                        var v = plToBoolean(val);
                        field.checked = v;
                        break;
                    }
                    default :
                    {
                        if ( field.type == undefined ) // maybe it's a radio button group?
                        {
                            var value = null;
                            for ( var c = 0; c < field.length; c ++ )
                            {
                                var button = field[c];
                                button.checked = false;
                                if (button.value == val)
                                {
                                    button.checked = true;
                                    break;
                                }
                            }
                        }
                        else throw new Error("setFormElementValue: unhandled type " + field.type );
                    }
                }
        }
    }

    //only support 2 level associative array structure.
    function plMergeDefaultProperties(props)
    {
        if ( !props ) props = {};
        for ( var n in pl_default_properties )
        {
            for ( var c in pl_default_properties[n] )
            {

            }
        }
    }


    //from dojo.cookie
    function plCookie(name, value, props)
    {
        //	summary:
        //		Get or set a cookie.
        //	description:
        // 		If one argument is passed, returns the value of the cookie
        // 		For two or more arguments, acts as a setter.
        //	name:
        //		Name of the cookie
        //	value:
        //		Value for the cookie
        //	props:
        //		Properties for the cookie
        //	example:
        //		set a cookie with the JSON-serialized contents of an object which
        //		will expire 5 days from now:
        //	|	dojo.cookie("configObj", dojo.toJson(config), { expires: 5 });
        //
        //	example:
        //		de-serialize a cookie back into a JavaScript object:
        //	|	var config = dojo.fromJson(dojo.cookie("configObj"));
        //
        //	example:
        //		delete a cookie:
        //	|	dojo.cookie("configObj", null, {expires: -1});
        var c = document.cookie;
        var m = [];
        if(arguments.length == 1)
        {
            /*
            //LS: original regex from dojo is  as below
            //var matches = c.match(new RegExp("(?:^|; )" + dojo.regexp.escapeString(name) + "=([^;]*)"));
            //am trying to pass a RE pattern as the name
            //plDebug(c);
            //
            //LS: this dojo stuff does not seem to bring all matches, so canning it.
            var ex ="(?:^|; )" + name + "=([^;]*)";
            plDebug("cookie expression=" + ex);
            var matches = c.match(new RegExp(ex));
            plDebug("matches length=" + matches.length);
            plDebug(matches);
            //return matches ? decodeURIComponent(matches[1]) : undefined; // String or undefined
            if ( matches )
            {
                for ( var i=1; i<= matches.length; i++)
                {
                    m[(i-1)] = decodeURIComponent(matches[i]);
                }
            }
            */
            var cookies = c.split(";");
            for ( var i=0; i< cookies.length; i++)
            {
                var cookie = cookies[i].split("=");
                var cn="";
                var cv="";
                if ( cookie.length > 0 ) cn = cookie[0].trim();
                if ( cookie.length > 1 ) cv = cookie[1].trim();
                if ( cn && cn.indexOf && cn.indexOf(name) == 0 )
                {
                    m.push(cv);
                }
            }
            return m;
        }
        else
        {
            props = props || {};
            // FIXME: expires=0 seems to disappear right away, not on close? (FF3)  Change docs?
            var exp = props.expires;
            if(typeof exp == "number"){
                var d = new Date();
                d.setTime(d.getTime() + exp*24*60*60*1000);
                exp = props.expires = d;
            }
            if(exp && exp.toUTCString){ props.expires = exp.toUTCString(); }

            value = encodeURIComponent(value);
            var updatedCookie = name + "=" + value, propName;
            for(propName in props){
                updatedCookie += "; " + propName;
                var propValue = props[propName];
                if(propValue !== true){ updatedCookie += "=" + propValue; }
            }
            document.cookie = updatedCookie;
        }
    }



    function plFormToMap(form)
    {
        var map = {};
        for ( var eleName in form.elements)
        {
            var ele = form.elements[eleName];
            if ( ! plIsEmpty(ele.name) && ele.type != 'button')
            {
                var val = plGetFormElementValue(ele);
                //val = isNaN(val)?('"' + val + '"'):val  ;
                //val = plIsEmpty(val)?'"':val;
                map[ele.name] = val;
            }
        }
        return map;
    }
	
	
	plAddHtmlNode = function( node, tag, value, attributes)
	{
		//placelogic.Util.removeChildrenFromNode(node); // remove any children
		var t = document.createElement(tag);
		//placelogic.Util.addHtmlAttributes(t, attributes);
		if (value) t.innerHTML = value; // GK 20071004 innerHTML won't work in IE if tag = input
		// TODO: IE8 chokes with mysterious error "Unknown Runtime Error" if you try to do invalide HTML like textarea with innerHTML or invalid nesting. 
		
		// this is the start of the IE8 alternative:
		/*
		if (value)
		{
			var sp = document.createElement("span");
			sp.innerHTML = value;
			t.appendChild(sp);
		}
		*/
		node.appendChild(t);
		if ( !plIsObjectEmpty(attributes))
			{
				for ( var a in attributes)
				{
					plAddHtmlAttribute( t, a, attributes[a]);
				}
			}
		return t;
	}
	
	plAddHtmlAttribute = function(node, attribName, attribValue  )
{
    // plDebug(" in addHtmlAttrubte, node, attribName, attribValue = " + node +","+ attribName+","+ attribValue)
    if ( !plIsEmpty(attribName))
    {
            //plDebug("adding attr=" + attribName + " value=" + attribValue);
            //LS: In IE setting a "style" attribute fails (Member not found), when using the node.setAttributeNode() method                     
            //var attr = document.createAttribute(attribName);
            //plDebug("aa 2");
            //
            //attr.value = attribValue
            //plDebug("aa 3");
            //
            //node.setAttributeNode(attr); 
            if (attribName == "onclick") plDebug("WARNING, attribName was onclick, probalby won't work in IE","warn");
            if ( attribName == 'style') // should test for IE here
            {
                //plDebug("Additionally setting style.cssText");
                node.style.cssText = attribValue;
            }
            else if ( attribName == 'class') // should test for IE hher
            {
                //plDebug("Additionally setting className");
                node.className = attribValue;
            } 
			
            node.setAttribute(attribName, attribValue);

            //plDebug("done adding attribute");
    }
}


//Merges a template with data from the object
//tpl - '<div>${id}</div><div>${name}</div>'
//obj - { id: 1, name: "test"}
//NOTE: only 1 level of data heirarchy is supported. ${profile.url} is not supported.
function plMergeTemplate(tpl, obj)
{
	var pat = /\$\{[a-zA-Z_0-9]*\}/g;
	var pos = tpl.match(pat);
	//console.debug(pos);
	var out = tpl;
	for ( var i=0; i<pos.length; i++)
	{
		var p = pos[i].substring(2);
		p = p.substring(0,(p.length-1));
		//console.debug(p);
		out = out.replace(pos[i],obj[p]);
		//console.debug(out);

	}
    return out;
}
/***  end of PLUtil **/
function plRemoveChildrenFromNode(node)
{
    if (!node)
    {
        return;
    }
    while (node.hasChildNodes())
    {
        node.removeChild(node.firstChild);
    }
}


if ( typeof plSerialize == "undefined" || !plSerialize) {
    function plSerialize(obj)
    {
        return JSON.stringify(obj);
    }
}

if ( typeof plDeSerialize == "undefined" || !plDeSerialize)
{
    function plDeSerialize(str)
    {
        return JSON.parse(str);
    }
}

function plToDelimtedText(hasharray, delim, entrydelim)
{
    var s = "";
    var first = true;
    for ( var k in hasharray)
    {
        s += (!first?entrydelim:"") + k + delim + escape(hasharray[k]);
        first = false;
    }
    return s;
}

function plToMiles(deg)
{
    var d = parseFloat(deg);
    if ( !d || isNaN(d)  || d <= 0 ) return 0;
    else return d * 69.1 ;
}



function plLoadDependencies ( timeoutMS, retries )
{
	alert("don't use plLoadDependencies");
	return;
	var complete = true;
	retries = plCoalesce ( retries, 10);
	for ( var i = 0 ; i < pl_dependencies.length; i ++ )
	{
		var dep = pl_dependencies[i];
		if ( ! dep.counter ) dep.counter = 0;
		plDebug("plLoadDependencies, dependency for " + dep.functionName + " was " + dep.objectName + ". functionWasExecuted = " + dep.functionWasExecuted + ". object is null? " +  plIsEmpty (obj ));
		if (dep.functionWasExecuted == true)
			{
				//plDebug("plLoadDependencies, dependency was was already loaded");
				// do nothing	
			}
		else //not loaded yet 
		{
           var allObjectsLoaded = true;
		   for ( var o in dep.objectName )
		   {
			   //plDebug("plLoadDependencies, trying object named " + dep.objectName[o]);
			   var oName = dep.objectName[o];
			   //plDebug("oname=" + oName);
			   try 
			   {
				var obj = eval (dep.objectName[o]);

			   	//plDebug("obj = " + obj + ", typeof = " + typeof(obj));
			   	if (typeof(obj) == "undefined" || obj == null)  { // obj==null handles some wierd doc.body cases
					allObjectsLoaded = false;
					plDebug("that object wasn't loaded, " + oName + ". Don't try any more in this dep");
					break;
				};
			   }
			   catch (err) 
			   { 
				plDebug("could not eval that object " + oName);
			   	allObjectsLoaded = false;
			   }
		   }
			if ( allObjectsLoaded == false )
				{ 	
					plDebug("this dependency isn't ready yet, so " + dep.functionName + "can't load yet");
					dep.counter += 1;
					if (dep.counter < retries ) complete = false; // limit to 10 trys
					else alert("error loading dependencies, please try again. dependency funtion name=" + dep.functionName);
				}
			else 
				{
					plDebug("dependency was already loaded but function not executed, yet, execute it now, " + dep.functionName);
					dep.functionWasExecuted = true;
					eval( dep.functionName + "()");
				}
		}
	}
	if ( complete == false ) // at least one was false, so set the timeout to re-run this function

	{
		plDebug("setting timeout to rerun plLoadDependencies in 1 second");
		setTimeout (plLoadDependencies, timeoutMS?timeoutMS:5000);
	}
}



function plTooltip( target, content ){ // target is a jquery object
	var tip = $("<div>").addClass("plTooltip").html("<div class='plTooltipContent'>" + content + "</div>").appendTo("body");
	target.mouseover( 
							   function ( kmouse ) 
							   { 
								tip.css ( {display: "none", left:kmouse.pageX+5, top:kmouse.pageY+5 } ).fadeIn(400);
							   } 
						   );
	target.mouseout( 
							   function ( ) 
							   {
								 tip.fadeOut(400);
							   } 
						   );
/*
	target_items.each(function(i){
		//$("body").append("<div class='"+classname+"' xid='"+name+i+"'><p>"+$(this).attr('title')+"</p></div>");
		//var my_tooltip = $("#"+name+i);

		$(this).removeAttr("title").mouseover(function(){
				my_tooltip.css({opacity:0.8, display:"none", left:kmouse.pageX+15, top:kmouse.pageY+15}).fadeIn(400);
		}).mousemove(function(kmouse){ 
				my_tooltip.css({left:kmouse.pageX+15, top:kmouse.pageY+15});
		}).mouseout(function(){
				my_tooltip.fadeOut(400);
		});
	});

*/
}
    /** ported From dojo library */
plListener = {
	// create a dispatcher function
	getDispatcher: function(){
		// following comments pulled out-of-line to prevent cloning them 
		// in the returned function.
		// - indices (i) that are really in the array of listeners (ls) will 
		//   not be in Array.prototype. This is the 'sparse array' trick
		//   that keeps us safe from libs that take liberties with built-in 
		//   objects
		// - listener is invoked with current scope (this)
		return function(){

                    try
                    {
			var ap=Array.prototype, c=arguments.callee, ls=c._listeners, t=c.target;
			// return value comes from original target function
			var r = t && t.apply(this, arguments);
			// make local copy of listener array so it is immutable during processing
			var lls = [];
                        for(var i in ls)
                        {
                            lls[i] = ls[i];
                        }			
			// invoke listeners after target function
			for( var i in lls){
				if(!(i in ap)){
					lls[i].apply(this, arguments);
				}
			}
			// return value comes from original target function
			return r;
                    }
                    catch (err)
                    {
                        plDebug("error in dispatcher::" + err.message, "warn");
                        throw err;

                    }
		}
	},
	// add a listener to an object
	add: function(/*Object*/ source, /*String*/ method, /*Function*/ listener){
		// Whenever 'method' is invoked, 'listener' will have the same scope.
		// Trying to supporting a context object for the listener led to 
		// complexity. 
		// Non trivial to provide 'once' functionality here
		// because listener could be the result of a dojo.hitch call,
		// in which case two references to the same hitch target would not
		// be equivalent. 
		source = source || window;
		// The source method is either null, a dispatcher, or some other function
		var f = source[method];
		// Ensure a dispatcher
		if(!f||!f._listeners){
			var d = plListener.getDispatcher();
			// original target function is special
			d.target = f;
			// dispatcher holds a list of listeners
			d._listeners = []; 
			// redirect source to dispatcher
			f = source[method] = d;
		}
		// The contract is that a handle is returned that can 
		// identify this listener for disconnect. 
		//
		// The type of the handle is private. Here is it implemented as Integer. 
		// DOM event code has this same contract but handle is Function 
		// in non-IE browsers.
		//
		// We could have separate lists of before and after listeners.
		return f._listeners.push(listener) ; /*Handle*/
	},
	// remove a listener from an object
	remove: function(/*Object*/ source, /*String*/ method, /*Handle*/ handle){
		var f = (source||window)[method];
		// remember that handle is the index+1 (0 is not a valid handle)
		if(f && f._listeners && handle--){
			delete f._listeners[handle];
		}
	}
};

// Multiple delegation for arbitrary methods.

// This unit knows nothing about DOM, but we include DOM aware documentation
// and dontFix argument here to help the autodocs. Actual DOM aware code is in
// event.js.

plConnect = function(/*Object|null*/ obj, 
						/*String*/ event, 
						/*Object|null*/ context, 
						/*String|Function*/ method,
						/*Boolean?*/ dontFix){
	// summary:
	//		`dojo.connect` is the core event handling and delegation method in
	//		Dojo. It allows one function to "listen in" on the execution of
	//		any other, triggering the second whenever the first is called. Many
	//		listeners may be attached to a function, and source functions may
	//		be either regular function calls or DOM events.
	//
	// description:
	//		Connects listeners to actions, so that after event fires, a
	//		listener is called with the same arguments passed to the orginal
	//		function.
	//
	//		Since `dojo.connect` allows the source of events to be either a
	//		"regular" JavaScript function or a DOM event, it provides a uniform
	//		interface for listening to all the types of events that an
	//		application is likely to deal with though a single, unified
	//		interface. DOM programmers may want to think of it as
	//		"addEventListener for everything and anything".
	//
	//		When setting up a connection, the `event` parameter must be a
	//		string that is the name of the method/event to be listened for. If
	//		`obj` is null, `dojo.global` is assumed, meaning that connections
	//		to global methods are supported but also that you may inadvertantly
	//		connect to a global by passing an incorrect object name or invalid
	//		reference.
	//
	//		`dojo.connect` generally is forgiving. If you pass the name of a
	//		function or method that does not yet exist on `obj`, connect will
	//		not fail, but will instead set up a stub method. Similarly, null
	//		arguments may simply be omitted such that fewer than 4 arguments
	//		may be required to set up a connection See the examples for deails.
	//
	//		The return value is a handle that is needed to 
	//		remove this connection with `dojo.disconnect`.
	//
	// obj: 
	//		The source object for the event function. 
	//		Defaults to `dojo.global` if null.
	//		If obj is a DOM node, the connection is delegated 
	//		to the DOM event manager (unless dontFix is true).
	//
	// event:
	//		String name of the event function in obj. 
	//		I.e. identifies a property `obj[event]`.
	//
	// context: 
	//		The object that method will receive as "this".
	//
	//		If context is null and method is a function, then method
	//		inherits the context of event.
	//	
	//		If method is a string then context must be the source 
	//		object object for method (context[method]). If context is null,
	//		dojo.global is used.
	//
	// method:
	//		A function reference, or name of a function in context. 
	//		The function identified by method fires after event does. 
	//		method receives the same arguments as the event.
	//		See context argument comments for information on method's scope.
	//
	// dontFix:
	//		If obj is a DOM node, set dontFix to true to prevent delegation 
	//		of this connection to the DOM event manager.
	//
	// example:
	//		When obj.onchange(), do ui.update():
	//	|	dojo.connect(obj, "onchange", ui, "update");
	//	|	dojo.connect(obj, "onchange", ui, ui.update); // same
	//
	// example:
	//		Using return value for disconnect:
	//	|	var link = dojo.connect(obj, "onchange", ui, "update");
	//	|	...
	//	|	dojo.disconnect(link);
	//
	// example:
	//		When onglobalevent executes, watcher.handler is invoked:
	//	|	dojo.connect(null, "onglobalevent", watcher, "handler");
	//
	// example:
	//		When ob.onCustomEvent executes, customEventHandler is invoked:
	//	|	dojo.connect(ob, "onCustomEvent", null, "customEventHandler");
	//	|	dojo.connect(ob, "onCustomEvent", "customEventHandler"); // same
	//
	// example:
	//		When ob.onCustomEvent executes, customEventHandler is invoked
	//		with the same scope (this):
	//	|	dojo.connect(ob, "onCustomEvent", null, customEventHandler);
	//	|	dojo.connect(ob, "onCustomEvent", customEventHandler); // same
	//
	// example:
	//		When globalEvent executes, globalHandler is invoked
	//		with the same scope (this):
	//	|	dojo.connect(null, "globalEvent", null, globalHandler);
	//	|	dojo.connect("globalEvent", globalHandler); // same

	// normalize arguments
	var a=arguments, args=[], i=0;
	// if a[0] is a String, obj was ommited
	args.push(plIsString(a[0]) ? null : a[i++], a[i++]);
	// if the arg-after-next is a String or Function, context was NOT omitted
	var a1 = a[i+1];
	args.push(plIsString(a1)||plIsFunction(a1) ? a[i++] : null, a[i++]);
	// absorb any additional arguments
	for(var l=a.length; i<l; i++){	args.push(a[i]); }
	// do the actual work
	return plDoConnect.apply(this, args); /*Handle*/
}

// used by non-browser hostenvs. always overriden by event.js
plDoConnect = function(obj, event, context, method){
	var l=plListener, h=l.add(obj, event, plHitch(context, method));
	return [obj, event, h, l]; // Handle
}

plDisconnect = function(/*Handle*/ handle){
	// summary:
	//		Remove a link created by dojo.connect.
	// description:
	//		Removes the connection between event and the method referenced by handle.
	// handle:
	//		the return value of the dojo.connect call that created the connection.
	if(handle && handle[0] !== undefined){
		plDoDisconnect.apply(this, handle);
		// let's not keep this reference
		delete handle[0];
	}
}

plDoDisconnect = function(obj, event, handle, listener){
	listener.remove(obj, event, handle);
}

// topic publish/subscribe

plTopics = {};

plSubscribe = function(/*String*/ topic, /*Object|null*/ context, /*String|Function*/ method){
	//	summary:
	//		Attach a listener to a named topic. The listener function is invoked whenever the
	//		named topic is published (see: dojo.publish).
	//		Returns a handle which is needed to unsubscribe this listener.
	//	context:
	//		Scope in which method will be invoked, or null for default scope.
	//	method:
	//		The name of a function in context, or a function reference. This is the function that
	//		is invoked when topic is published.
	//	example:
	//	|	dojo.subscribe("alerts", null, function(caption, message){ alert(caption + "\n" + message); });
	//	|	dojo.publish("alerts", [ "read this", "hello world" ]);																	

	// support for 2 argument invocation (omitting context) depends on hitch
	return [topic, plListener.add(plTopics, topic, plHitch(context, method))]; /*Handle*/
}

plUnsubscribe = function(/*Handle*/ handle){
	//	summary:
	//	 	Remove a topic listener. 
	//	handle:
	//	 	The handle returned from a call to subscribe.
	//	example:
	//	|	var alerter = dojo.subscribe("alerts", null, function(caption, message){ alert(caption + "\n" + message); };
	//	|	...
	//	|	dojo.unsubscribe(alerter);
	if(handle){
		plListener.remove(plTopics, handle[0], handle[1]);
	}
}

plPublish = function(/*String*/ topic, /*Array*/ args){
	//	summary:
	//	 	Invoke all listener method subscribed to topic.
	//	topic:
	//	 	The name of the topic to publish.
	//	args:
	//	 	An array of arguments. The arguments will be applied 
	//	 	to each topic subscriber (as first class parameters, via apply).
	//	example:
	//	|	dojo.subscribe("alerts", null, function(caption, message){ alert(caption + "\n" + message); };
	//	|	dojo.publish("alerts", [ "read this", "hello world" ]);	

	// Note that args is an array, which is more efficient vs variable length
	// argument list.  Ideally, var args would be implemented via Array
	// throughout the APIs.
        plDebug("in publish");
        //debugger;
        plDebug("plPublish topic=" + topic);
        //plDebug(args);
        try
        {
            var f = plTopics[topic];
            var a = ( typeof args != "undefined" && args)? [args]: []; //LS : args needs to be a [] array, but I prefer a hash array for our programs.
            if(f){
                plDebug ("applying fn=");
		f.apply(this, a ); //args||[]);
            }
            else
            {
                plDebug("plPublish topic listsner f not found");
            }
        }
        catch (err)
        {
            plDebug ("Error in plPublish:" + err.message);
        }
}

plConnectPublisher = function(	/*String*/ topic,
									/*Object|null*/ obj, 
									/*String*/ event){
	//	summary:
	//	 	Ensure that everytime obj.event() is called, a message is published
	//	 	on the topic. Returns a handle which can be passed to
	//	 	dojo.disconnect() to disable subsequent automatic publication on
	//	 	the topic.
	//	topic:
	//	 	The name of the topic to publish.
	//	obj: 
	//	 	The source object for the event function. Defaults to dojo.global
	//	 	if null.
	//	event:
	//	 	The name of the event function in obj. 
	//	 	I.e. identifies a property obj[event].
	//	example:
	//	|	dojo.connectPublisher("/ajax/start", dojo, "xhrGet");
	var pf = function(){ plPublish(topic, arguments); }
	return (event) ? plConnect(obj, event, pf) : plConnect(obj, pf); //Handle
};


plHitchArgs = function(scope, method /*,...*/){
    var pre = plToArray(arguments, 2);
    var named = plIsString(method);
    return function(){
        // arrayify arguments
        var args = plToArray(arguments);
        // locate our method
        var f = named ? (scope||window)[method] : method;
        // invoke with collected args
        return f && f.apply(scope || this, pre.concat(args)); // mixed
    } // Function
}

	plHitch = function(/*Object*/scope, /*Function|String*/method /*,...*/){
		//	summary:
		//		Returns a function that will only ever execute in the a given scope.
		//		This allows for easy use of object member functions
		//		in callbacks and other places in which the "this" keyword may
		//		otherwise not reference the expected scope.
		//		Any number of default positional arguments may be passed as parameters
		//		beyond "method".
		//		Each of these values will be used to "placehold" (similar to curry)
		//		for the hitched function.
		//	scope:
		//		The scope to use when method executes. If method is a string,
		//		scope is also the object containing method.
		//	method:
		//		A function to be hitched to scope, or the name of the method in
		//		scope to be hitched.
		//	example:
		//	|	dojo.hitch(foo, "bar")();
		//		runs foo.bar() in the scope of foo
		//	example:
		//	|	dojo.hitch(foo, myFunction);
		//		returns a function that runs myFunction in the scope of foo
		//	example:
		//		Expansion on the default positional arguments passed along from
		//		hitch. Passed args are mixed first, additional args after.
		//	|	var foo = { bar: function(a, b, c){ console.log(a, b, c); } };
		//	|	var fn = dojo.hitch(foo, "bar", 1, 2);
		//	|	fn(3); // logs "1, 2, 3"
		//	example:
		//	|	var foo = { bar: 2 };
		//	|	dojo.hitch(foo, function(){ this.bar = 10; })();
		//		execute an anonymous function in scope of foo

		if(arguments.length > 2){
			return plHitchArgs.apply(d, arguments); // Function
		}
		if(!method){
			method = scope;
			scope = null;
		}
		if(plIsString(method)){
			scope = scope || window;
			if(!scope[method]){ throw(['plHitch: scope["', method, '"] is null (scope="', scope, '")'].join('')); }
			return function(){ return scope[method].apply(scope, arguments || []); }; // Function
		}
		return !scope ? method : function(){ return method.apply(scope, arguments || []); }; // Function
	}



// PL stuff

var pl_messages = {};
var pl_publishQueue = []; //[{topic:"", args: {}},..]messages ready to be published, but waiting to plPublish function

function plSubscribedFunctionFactory(ind)
{
    plDebug("plSubscribedFunctionFactory(" + ind +") ");
    var fn = pl_loadConfig.subscriptions[ind].functionName;
    /*return function()
    {
       var fstr = "";
       try
       {
           plDebug("8888****88888");
           plDebug(fn);
           //fstr = " { debugger; " + fn  + "}";
           //fstr = "{ alert ('hi');}";
           //var f = eval(fstr );
           plRunAdhocScript(fn);
       }
       catch (e)
       {
            plDebug("coukd not eval fn=" + fstr + " " + e.message);
       }
       //if ( f) f.apply();
    }*/
    return function(args){ plRunAdhocScript(fn, args);};
}

function plRunAdhocScript(fstr, args)
{
    plDebug("in run adhoc fnstr=" + fstr);
    if ( !pl_messages["ready_4_subscribe"]) return;
    try
    {
        //eval("{" + fstr + "}");
        var f = new Function("args", fstr);
        if ( f) f.apply(this, [args]);
    }
    catch (err)
    {
        plDebug("Error in: plRunAdhocScript: " + fstr + " msg=" + err.message, "warn");
    }
}


function plDomReadyFnFactory(ele, topic, args)
{
    //plDebug("plDomReadyFnFactory(" + ele +") ");
    var en = ele;
    var et = topic;
    var ea = args || {};
    return function()
    {
        //alert('1::' + en + " et" + et );
        jQuery(en).ready(
            function()
            {
                //alert ("et=" + et);
                if ( plPublish)
                {
                     //plDebug("^^^^^^^^^^^^^^publishing t=" + et + " args=" + ea.message);
                     try
                     {
                        pl_messages[ea.message] = true;
                        plPublish(et, ea); //this is not being invoked for some reason
                        //debugger;
                     }
                     catch (e)
                     {
                         alert ('plDomREadyFnFactory:get: ' + e.message);
                     }
                }
                else
                {
                    plDebug("^^^^^^^^^^^^^^^^^^^publishing to queue");
                    pl_publishQueue.push({ topic: et, args: ea} );
                }
            }
        );
    }
}

function plParseLoadConfig( loadConfig)
{
   //Subscribe to the "load" topic.
   plSubscribe( "load", null, plOnLoadMessage); //listen to load topics
   //
   //handle subscriptions
   if ( typeof ( loadConfig.subscriptions != "undefined"))
   {
       for ( var i=0; i< loadConfig.subscriptions.length; i++)
       {
            plDebug("subscribing to topic " + loadConfig.subscriptions[i].topic + " func=" + loadConfig.subscriptions[i].functionName );
            var fn = plSubscribedFunctionFactory(i) ;
            plDebug("listening fn=" + fn);
            plSubscribe( loadConfig.subscriptions[i].topic, window, fn );
       }
       pl_messages["ready_4_subscribe"] = true;
   }
   //
   ////
   //
   //handle domReady
   if ( typeof ( loadConfig.domReadyPublish != "undefined"))
   {
       for ( var i=0; i< loadConfig.domReadyPublish.length; i++)
       {
           var f = plDomReadyFnFactory(loadConfig.domReadyPublish[i].element, loadConfig.domReadyPublish[i].topic, loadConfig.domReadyPublish[i].args);
           //plDebug("domReady handler: " + f);
           f.apply();
       }
   }

   
   //handle polling events, until load is complete
   if ( typeof loadConfig.poll != "undefined" && loadConfig.poll && loadConfig.poll.interval > 0 )
   {
        pl_loadCheckInterval = setInterval("plOnLoadPoll()", loadConfig.poll.interval);
   }

}


var pl_loadCheckInterval;
var pl_loadTimeCounter = 0;

function plOnLoadPoll()
{
    pl_loadTimeCounter += pl_loadConfig.poll.interval;
    if ( pl_loadTimeCounter > pl_loadConfig.poll.maxTime)
    {
        //stop the bg thread
        clearInterval(pl_loadCheckInterval);

        if ( typeof pl_loadConfig.conditionalPublish != "undefined")
        {
            //check if there are any more conditionalPublish elements that have not fired.
            var reqdItems = ''; //failed items whose fail=true
            var inheritedItems = ''; //failed items w/o fail attribute (inheriting
            for ( var i=0; i< pl_loadConfig.conditionalPublish.length; i++)
            {
                var itm = pl_loadConfig.conditionalPublish[i];
                if ( itm.status == 0 ) //item failed
                {
                    if ( typeof itm.failOnError == "undefined") //inherit from pl_loadConfig.poll.failOnMaxTime
                    {
                        if ( pl_loadConfig.poll.failOnMaxTime == true ) inheritedItems += ',' + (itm.key||i);
                    }
                    else if ( itm.failOnError == true) //required item
                    {
                        reqdItems += ',' + (itm.key||i);
                    }
                    //else ignore
                }
            }
            if ( !plIsEmpty(reqdItems))
                throw new Error("Failed loading dependencies (" + reqdItems + "). Reload page and try again");
            else if ( !plIsEmpty(inheritedItems) )
                throw new Error("Failed loading dependencies (" + inheritedItems + "). Reload page and try again");
        }
        return;
    }
    //plProcessPublishQueue();
    plProcessConditionalPublish();
}

function plGetConditionalPublishItemByKey(key)
{
    for ( var i=0; i< pl_loadConfig.conditionalPublish.length; i++)
    {
        if ( typeof pl_loadConfig.conditionalPublish[i].key != "undefined" && pl_loadConfig.conditionalPublish[i].key == key )
        {
            return pl_loadConfig.conditionalPublish[i];
        }
    }
    return null;
}

function plGetConditionalPublishItemByTopic(topic)
{
    for ( var i=0; i< pl_loadConfig.conditionalPublish.length; i++)
    {
        if ( pl_loadConfig.conditionalPublish[i].publish.topic == topic )
        {
            return pl_loadConfig.conditionalPublish[i];
        }
    }
    return null;
}

function plProcessPublishQueue()
{
    var m;
    //plDebug("que is " + pl_publishQueue.length);
    if ( !plPublish) return;
    while ( ( m=pl_publishQueue.shift()) )
    {
        plPublish(m.topic, m.args||{});
    }
}

function plOnLoadMessage(args)
{
    //plDebug("plOnLoadMessage message=" + args.message);
    //plDebug(args);
    pl_messages[args.message] = true;
    //process the multi condition
    plProcessConditionalPublish();
}

function plProcessConditionalPublish()
{
    if ( typeof pl_loadConfig.conditionalPublish == "undefined" ) return;
    
    for ( var i=0; i< pl_loadConfig.conditionalPublish.length; i++)
    {
        var item = pl_loadConfig.conditionalPublish[i];
        if ( item.condition )
        {
            if ( "once"==item.fire && item.status == 1 )
            {
                //plDebug("item " + i + " has already fired");
                continue;
            }
            var e = false;
            try
            {
               e = eval("(" + item.condition + ")");
            }
            catch (err)
            {
                plDebug("eval conditon failed key:" + item.key + " condition=" + item.condition + e.message);
                if ( typeof (item.fail) != "undefined" && item.fail == true )
                    throw new Error("Condtional publish failed for the key:" + item.key);
            }
            //plDebug(" e=" + e + " condition=" + item.condition);
            if ( e == true )
            {
                item.status = 1;
                plDebug("condition=" + item.condition + " passed, so publish topic=" + item.publish.topic);
                plPublish.call(window, item.publish.topic, item.publish.args);
            }
            else
            {
                plDebug("condition=" + item.condition + " failed");
            }
        }
    }
}

if ( typeof  pl_loadConfig != "undefined" &&  pl_loadConfig)
{
    plParseLoadConfig( pl_loadConfig);
}



                            var googleMapsApiKey = "ABQIAAAAon8cbBbv00suw2ir0dBArhSQmCDGynfKfHhl57NRDCOs1hw9FxRbRGHPTeH4r7ERp_pKN5Xv-Eng5Q";
                                                    plLoadScript("/res/openLayers/OpenLayers.js", {onsuccess: function(){ plPublish("load", { message: "loaded_openlayers"} );}});
        /***  Formula stuff **/
    //Global variables
    // ==========================================================
    //
    //
    //var pl_zone = {"type":"Polygon","coordinates":[[[-121.92,37.42],[-121.92,38.13],[-122.64,38.13],[-122.64,37.42],[-121.92,37.42]]]}; //pl_zone is set in the customer xml file
    //in 900913 srid:
    //var pl_zone = {"type":"Polygon","coordinates":
    //        [[[-13572072.3175159,4497812.18665278],[-13572072.3175159,4597806.76524309],[-13652222.3508871,4597806.76524309]
    //            ,[-13652222.3508871,4497812.18665278],[-13572072.3175159,4497812.18665278]]] };
    var pl_zone = null;

    var pl_map;
    var pl_mapBaseLayer;
    var pl_mapFormulaLayer;
    var pl_mapNearbyLayers = {};
    //var pl_selectControl;
    //var pl_selectedFeature;
    var pl_resultsDataTable = null;
    var pl_callprops ={};
    var pl_TRACK_BEHAVIOR_COOKIE_PREFIX = "PL_FORMULA_";
    var pl_SAVED_ITEM_COOKIE_PREFIX = "PL_ITEM_";
	var pl_zoneMap;


	/*
	var pl_criteriaInFormula = [];
	var pl_formula = {
		setInitialValue : function (i,cr) { this.items[i] = { value: 0, fsid: cr.fsid, name: cr.name  } }
		, setValue : function (i,a) {
			if ( this.items[i] )  this.items[i].value = a
			else this.items[i] = { value: a }
			}
		, items : []
		};
		*/
		// need to override these three depending the UI widget used
function plShowProcessing () {plDebug("plShowProcessing should be overwritten")}
function plShowDialog () { plDebug("plShowDialog should be overwritten")}
function plShowPanel () {plDebug("plShowPanel should be overwritten")}
function plHideProcessing () {plDebug("plHideProcessing should be overwritten")}
function plHideDialog () { plDebug("plHideDialog should be overwritten")}
function plHidePanel () {plDebug("plHidePanel should be overwritten")}

var pl_zoneBuilder = {
		//updateZoneMap : function () { plDebug("WARNING: this function pl_zoneBuilder.updateZoneMap() needs to be overwritten", "warn") }
		onComplete : function () {} // overwrite this, maybe to close dialog, update zone map, etc.
		, containerNames :[ "plEleZoneBuilderStepOne"	,"plEleZoneBuilderStepMap", "plEleZoneBuilderStepAddress", "plEleZoneBuilderStepRegionTypes", "plEleZoneBuilderStepRegionList"]
		/*
		, add : function ( containerName, container ) {
			this[containerName] = container;
			this.containerNames.push(containerName)
			}
			*/
		, beforeHide : function () { 
			//plDebug("pl_zoneBuidler.beforeHide()"); 
			plById("plEleZoneBuilderStepMap").style.display = "none"; 
			return true;
			} // have to hide this layer or it still shows after hiding the dialog

		, showContainerByName : function ( containerName ) {
				plDebug("pl_zoneBuilder.showContainerByName containerName=" + containerName);
				for ( var c in this.containerNames )
					{
					var n = this.containerNames[c];
					var e = document.getElementById(n);
					if ( containerName == n) //this[n].show();
					{
						plDebug("show " + n);
						//e.style.zIndex = "2000";
						e.style.display = "block"; // map layer might be hidden
					}
					else // this[n].hide();
					{ 
						plDebug("hide " + n);
						//e.style.zIndex = "1";
						e.style.display="none";
					}
					}
				//pscoreDemo_dialogs.overlayManager.find("plEleZoneBuilder").center();
				}
		, defaultHTML: '<div id="plEleZoneBuilderContent">'
						+ '<div id="plEleZoneBuilderStepOne" class="plEleZoneBuilderSteps">'
							+ '<div>Select a method to build a Zone:</div>'
							+ '<div style="margin-left: 10px;">'
								+ '<div><button onClick="pl_zoneBuilder.buildZoneOnMap();" class="plZoneBuilderButton">Draw on a map</button></div>'
								+ '<div><button onClick="pl_zoneBuilder.buildZoneFromAddress();" class="plZoneBuilderButton">Enter street address</button></div>'
								+ '<div><button onClick="pl_zoneBuilder.buildZoneFromPredefined();" class="plZoneBuilderButton">Choose a region</button></div>'
							+ '</div>'
						+ '</div>'
						+ '<div id="plEleZoneBuilderStepMap" class="plEleZoneBuilderSteps">'
							+ '<div id="plEleZoneMapContainer" style="width: 400px; height: 400px"></div>'
							+ '<div id="plEleZoneBuilderConfirm" style="display: none"><button onClick="pl_zoneBuilder.zoneConfirmed()" class="plZoneBuilderButton">Confirm Zone</button></div>'
							//+ '<!--<div id="plEleZoneBuilderMapMessage"></div><div id="plEleZoneBuilderMapOption" style="display:none"><label for=""></label><input id="" type="text" size="50" maxlength="250" /><button onClick=""></button></div><div id="plEleZoneToolbar"><button onClick="pl_zoneBuilder.plBuildZoneMainOptions()">back</button><button onClick="pl_zoneBuilder.plBuildZoneCancel()">cancel</button></div>-->'
						+ '</div>'
						+ '<div id="plEleZoneBuilderStepAddress" class="plEleZoneBuilderSteps">'
							+ '<div><label for="plEleAddress">Address</label><input name="plEleAddress" id="plEleAddress" value="" /></div>'
							+' <div><label for="plEleRadius">Radius (miles)</label><input name="plEleAddress" id="plEleRadius" value="" /></div>'
							+ '<button onClick="pl_zoneBuilder.buildZoneSubmitAddress();" class="plZoneBuilderButton">Submit</button>'
						+ '</div>'
						+ '<div id="plEleZoneBuilderStepRegionTypes" class="plEleZoneBuilderSteps">'
							+ '<div>What type of region?</div>'
							+ '<div id="plEleZoneBuilderRegionTypes">'
							+ 	'<ul>'
							+	  '<li><a href="javascript:void(0)" onClick="pl_zoneBuilder.showRegions(102)">Counties (CA only)</a></li>'
							+	  '<li><a href="javascript:void(0)" onClick="pl_zoneBuilder.showRegions(103)">Zip codes (CA only)</a></li>'
							+	  '<li><a href="javascript:void(0)" onClick="pl_zoneBuilder.showRegions(474)">Neighborhoods (CA only)</a></li>'
							+   '</ul>'
							+ '</div>'
						+ '</div>'
						+ '<div id="plEleZoneBuilderStepRegionList" class="plEleZoneBuilderSteps">'
							+ '<div>Choose a region:</div>'
							+ '<div>'
							+ 	'<div id="plEleZoneBuilderRegionResults"></div>'
							+ '</div>'
						+ '</div>'
					+ '</div>'
		, initialize : function ()
			{
				if ( this.isInitialized == true )
				{
					this.showContainerByName("plEleZoneBuilderStepOne");
				}

				else
				{
					document.getElementById("plEleZoneBuilder").innerHTML = this.defaultHTML;
					this.showContainerByName( this.containerNames[0] ); // show first container;
				}
				this.isInitialized = true;
			}

		, initializeMap : function ()
		{
			if ( pl_zoneMap )
			{ // reuse it
			var num = pl_zoneMap.getNumLayers();
			for (var j=1; j<num; j++)
				{
					pl_zoneMap.removeLayer( pl_zoneMap.layers[1] );
				}
			}
			else
			{ // create it
				var defaultProps = pl_properties.defaultRunProperties;
				var props = {
					map: {  enabled : true
						, container: "plEleZoneMapContainer"
						, layerName: "Zone"
						, zoom: defaultProps.map.zoom
						, defaultCenter : { lng: defaultProps.map.defaultCenter.lng, lat: defaultProps.map.defaultCenter.lat }
						, mapObjectName : "pl_zoneMap"
						, setBaseLayer : defaultProps.map.setBaseLayer
						, WMSLayerUrl : defaultProps.map.WMSLayerUrl
					}
				}
				plLoadBaseMap ( props );
			}
			this.showConfirmButton(false)
		}
		, buildZoneOnMap : function ()
		{
			this.showContainerByName( "plEleZoneBuilderStepMap" );
			this.initializeMap ();
			var drawLayer = new OpenLayers.Layer.Vector("drawLayer");
			pl_zoneMap.addLayer(drawLayer);
			var polygon = new OpenLayers.Control.DrawFeature(drawLayer, OpenLayers.Handler.Polygon );
			polygon.featureAdded = this.buildZoneOnMapFeatureAdded;
			pl_zoneMap.addControl( polygon );
			polygon.activate();
		}
		, buildZoneOnMapFeatureAdded : function ( polygonFeature )
		{
			// scope isn't "pl_zoneBuilder" any more.
			pl_zoneBuilder.confirmZone ( polygonFeature.geometry );
		}
		
		, buildZoneFromAddress : function ()
		{
			this.showContainerByName( "plEleZoneBuilderStepAddress" );
		}
		, buildZoneSubmitAddress : function ()
		{
			var addr = plById("plEleAddress").value;
			plDebug(addr);
			pl_properties.geoCoder.call ( this, addr, this.buildZoneGeocodeCallback);
		}
		, buildZoneGeocodeCallback : function ( geoCodedLocation )
		{
			plDebug("geoCodedLocation="+geoCodedLocation.toString());
			var point = new OpenLayers.Geometry.Point ( geoCodedLocation.lng(), geoCodedLocation.lat() );
			var newPoint = plReprojectGeometry ( point, "4326", "900913" );
			var radiusInMiles = plById("plEleRadius").value;
			//var conversionFactor = 69.1; // defaults to miles, 1 degree = 69.1 miles
			var conversionFactor = 0.000621371192; // 1 meter = 0.000621371192 miles
			var r = radiusInMiles / conversionFactor;
			var square = OpenLayers.Geometry.Polygon.createRegularPolygon ( newPoint, r, 4, 0 );
			pl_zoneBuilder.confirmZone( square );
		}

		, buildZoneFromPredefined : function () 
		{ 
			this.showContainerByName( "plEleZoneBuilderStepRegionTypes" );
		}
		, regionsLimit : 10 // show this many at a time
		, regionsFsid : null
		, regionsFeatures : null // so we can get the geometry after they select
		, showRegions : function ( fsid, offset )
		{
			plDebug("showRegions("+fsid+")");
			pl_zoneBuilder.regionsFsid = fsid;
			var o = plCoalesce(offset, 0); // offset defaults to zero
			if ( fsid > 0 )
			{


				// get the features in this FS
				var buffer = 1000; // miles
				var params = "";
				//	var zone = JSON.stringify ( pl_zone);
				var zone = JSON.stringify( { "type": "Point", "coordinates": [pl_properties.center.lng, pl_properties.center.lat ] });
				var tpl ='<div javascript="void(0)" onClick="pl_zoneBuilder.onRegionSelected(${index})">${name}</div>';
				var props = { 
					"container" : "plEleZoneBuilderRegionResults"
					, "template":tpl
					, "nextFunctionName":"pl_zoneBuilder.showNextRegions"
					, "offset":o
					, "limit":pl_zoneBuilder.regionsLimit
					}
				plCallNearbyService ( "pl_zoneBuilder.showRegionsCallback", zone, "geojson", buffer, "mile", fsid, params, "distance", pl_zoneBuilder.regionsLimit, o, props )

				//this.showContainerByName("plEleZoneBuilderStepRegionList");
				//plEleZoneBuilderRegions
			}
		}
		, onRegionSelected :function (fid)
		{ 
			var feat = pl_zoneBuilder.regionsFeatures [fid];
			if (feat && feat.geometry)
			{
				var olGeoJson = new OpenLayers.Format.GeoJSON();
				var geom = olGeoJson.read(feat.geometry, "Geometry");
				pl_zoneBuilder.confirmZone(geom);
			}
		}
		, showRegionsCallback : function (resp, callid)
		{
				pl_zoneBuilder.regionsFeatures = resp.response.content.features;
				plRenderFeaturesAsBlog(resp, callid);
				pl_zoneBuilder.showContainerByName("plEleZoneBuilderStepRegionList");
		}
		, showNextRegions : function ( offset )
		{
			pl_zoneBuilder.showRegions ( pl_zoneBuilder.regionsFsid, offset)
		}

		, confirmZone : function ( geometry )
		{
			this.showContainerByName("plEleZoneBuilderStepMap");
			plDebug("in confirmZone, geometry = " + geometry);
			this.initializeMap();
			var confirmLayer = new OpenLayers.Layer.Vector("confirmLayer");
			pl_zoneMap.addLayer(confirmLayer);
			var polygon = new OpenLayers.Feature.Vector  ( geometry );
			confirmLayer.addFeatures([polygon]);
			pl_zoneMap.zoomToExtent ( geometry.bounds );
			this.showConfirmButton();
		}
		, showConfirmButton : function ( vis ) {
			var state = vis == false ? "none" : "block";
			plById("plEleZoneBuilderConfirm").style.display = state;
			}
		
		
		, zoneConfirmed : function ()
		{
			plDebug("in zoneConfirmed()");
			//plById("plEleZoneBuilderStepMap").style.display = "none";
			var confirmLayer = pl_zoneMap.getLayersByName("confirmLayer")[0];
			var feats = confirmLayer.features;
			//debugger;
			var pcode = confirmLayer.projection.projCode.split(":")[1]; // probably 900913
			var geometry = feats[0].geometry;
			var geometry_proj = feats[0].geometry;
			
			
			if ( pcode != pl_properties.geometrySrid ) 
				{
					geometry_proj = plReprojectGeometry (geometry, pcode, pl_properties.geometrySrid);
				}
				
			//geometry_proj = plReprojectGeometry (geometry, pcode, 4326); // TODO this is temporary workaround
				
			var olGeoJson = new OpenLayers.Format.GeoJSON();
			var geomGeoJson = olGeoJson.write ( geometry_proj ) ; // make it geojson, as a string
			var olJson = new OpenLayers.Format.JSON ();
			var geom = olJson.read (geomGeoJson);
			pl_zone = geom;
			
			//pl_zone_obj = { featurecoll: feats, projcode: pcode } // store it as OL feature collection too for use on the map w/o re-creating
			
			this.onComplete ( feats, pcode ); // return a feature collection and projection code for use on another map
		}
	};
	
    function plZoneSerialize(z)
    {
        if ( ! pl_zone ) pl_zone = pl_properties["zone"];
        var zone = ( z ? z : pl_zone);
        /*return '<context option="' + zone.option +'" filterSqlText="' + zone.filterSqlText
                    + '" circle="' + zone.circle + '" extent="' + zone.extent
                    + '" regionsFeatureSetId="' + zone.regionsFeatureSetId + '" addressRadiusUnit="' + zone.addressRadiusUnit
                    + '" address="' + zone.address + '" addressRadius="' + zone.addressRadius + '" addressRadiusConverted="' + zone.addressRadiusConverted
                    + '" addressLat="' + zone.addressLat + '" addressLng="' + zone.addressLng + '" regionsFeatureId="' + zone.regionsFeatureId
                    + '">' + zone.geometry + '</context>'

        */
       return '<context format="geojson"><![CDATA[' + escape(JSON.stringify(zone)) + ']]></context>';
    }

    // end of global variables
    // ==========================================================
    //
    //


   //generates and runs a formula
   function plRun(xml, resultsHandler, props, transportString)
   {
       plDebug("in plRun");
        if ( !xml ) xml = pl_properties.formulaBuilder.call(window);
        plClearError();
        plShowProcessing(true);
		plRunThisFormula(xml, (resultsHandler?resultsHandler: "plRunFormulaCallback")
            , (props?props: pl_properties.defaultRunProperties), transportString );
	}

	//calls the service to run a Formula
   function plRunThisFormula(fXml, cb, props, transportString)
	{
		var cid = plGetNewCallId();
        if ( true == pl_properties.trackBehavior.enabled  && pl_properties.trackBehavior.handler )
        {
            plDebug("got to track formula");
            pl_properties.trackBehavior.handler.call(window, cid, fXml );
        }
        else
        {
            plDebug("ignoring track formula");
        }
		/*
        var url = pl_properties["servicesUrlBase"] + '/formula/json/0/run.action?formula=' + escape(fXml)
			+ '&callid=' + cid + '&callback=' + cb  +'&apikey=' + pl_properties.apiKey + '&version=1.0';
        plDebug("url=" + url);
		plCallXDomain( cid, url , props, transportString);
		
		var params ={
			"callback":cb
			,"uid":null
			,"formula":fXml
			,"props":props
		}
		*/
		props.callback = cb;
		props.formula=fXml;
		PL.restapi.ranking.run(props);
		
	}



	//callback
	function plRunFormulaCallback(data, callid)
	{
        plDebug("in plRunFormulaCallback: callid:" + callid);
        plShowProcessing(false);
        var props = pl_callprops["call_" + callid];
		//var props = params.props;
        if ( !props) props = {};
        plDebug("check");
        if ( data && data.response && data.response.status && 'SUCCESS'== data.response.status.code )
        {
            //call succeeded.
            //get the properties array for the callid from the JS global var
            var handler = (props.resultsHandler ? props.resultsHandler : pl_properties.runFormulaCallback);
            plDebug("b4 show results");
            //plDebug(props);
            if ( handler ) handler.call( window, data.response.content, callid, props);
            else { plDebug("no resultsHandler for callback","warn");}
        }
        else
        {
            plDebug("error");
            var eh = ( props.errorHandler ? props.errorHandler : plErrorHandler );
            eh.call("Formula failed", data.response.status.message, callid, props);
        }
	}

    function plShowResults(data, callid, props)
    {
        //data.features array will contain the data
        plDebug("in plShowResults");
        //plDebug(data);
        //plDebug(props);
		//var fcoll = plToFeatureCollection(data);
		var fcoll = data;
        if ( props && props.map && props.map.enabled == true && props.map.handler )
        {
            plDebug("b4 map handler");
            //plDebug(props);
            //temp
			//var fcoll_as_ol = plGeoJson2OpenLayers ( fcoll, "FeatureCollection");
            props.map.handler.call(window, fcoll, props, true, "geojson"); //display map
        }
        else
        {
            plDebug("props.map is disabled or no map handler, so ignoring map");
        }
		if ( props && props.grid && true == props.grid.enabled && props.grid.handler )
        {
            var d = plById((props.grid.container?props.grid.container:"plEleResults"));
            //var features = (data && data.features ? data.features.items : [] );
            if ( d )
            {
                props.grid.handler.call(window, d, fcoll, props);
            }
        }
        else
        {
            plDebug("grid is not enabled");
        }

    }



    function plGetCriteriaByKey(ckey)
    {
        for ( var i=0; i< pl_criteria.length; i++)
        {
            if ( pl_criteria[i].key == ckey ) return pl_criteria[i];
        }
    }

    //experimental: genereric UI for filters
    function plRenderFilters(divId, mainIndex)
    {
        var container = plById(divId);
        if ( !container) return;
        var mid = 0;
        if ( mainIndex && mainIndex >= 0 && mainIndex < pl_main.length )
        {
            mid = mainIndex;
        }
        var html = '';
        html += '<div>Filter by:</div>'
             +      '<div class="plEleFilterGroup">'
             +          '<form id="plEleFilterForm" action="javascript:void(0)" >';
        for ( var pname in pl_main[mid].params)
        {
            var p = pl_main[mid].params[pname];
            html += '<div class="plEleFilterItem">';
            html += plGetHtmlInputForParameter(p);
            html += '</div>';
        }
        container.innerHTML = html;
    }

    //experimental: genereric UI for filters
    function plGetHtmlInputForParameter(p)
    {
        var html = '<label for="plEleFilter_' + p.name + '">' + plCoalesce(p.label, p.name) + '</label>';
        if ( p.options && p.options.length > 0 )
        {
            html += '<select id="plEleFilter_' + p.name + '" name="plEleFilter_' + p.name + '">'
                       + '<option value="">Any</option>';
            for ( var oi = 0; oi < p.options.length; oi++)
            {
               html += '<option value="' + p.options[oi].code + '">' + p.options[oi].label + '</option>';
            }
            html += '</select>';
        }
        else
        {
           html += '<input type="text" name="plEleFilter_' + p.name + '" id="plEleFilter_' + p.name + '" />';
        }
        return html;
    }

    //LS, this loop is incorrect, use the regulat for loop for arrays
	/*function plGetCriteriaByFormulaPosition ( pos )
	{
		for ( var i in pl_criteria )
		{
			if ( pl_criteria[i].formulaPosition == pos ) return pl_criteria[i];
		}
	}*/


		
    //transform from PL structure to OpenLayers GeoJson structure
	// NO LONGER USED since API returns geojson now 2009.05.01
	/*
    function plToFeatureCollection(data)
    {
       var featurecollection = {
                "type": "FeatureCollection",
                "features": []
           };
       var features = (data && data.features ? data.features.items : [] );
       for ( var i = 0; i< features.length; i++)
       {
            featurecollection.features.push(
                  {
                    "geometry": features[i].geometryJson
                    , "type": "Feature"
                    , "properties":
                    {
                          name : features[i].name
                        , description : features[i].description
                        , overallScore : features[i].attributes.rank
						, rank : i + 1
						, score1 : features[i].attributes.score1
						, score2 : features[i].attributes.score2
                    }
                }
            );
       }
       return featurecollection;
    }
    */
	
    function plLoadBaseMap(props)
    {
		plDebug("plLoadBaseMap");
        //construct a Map object and add a base layer
        if ( !props || !props.map || !props.map.enabled ) props = pl_properties.defaultRunProperties;
        var options = {};
        if ( pl_properties["geometrySrid"] == 900913)
        {
             options = { projection: new OpenLayers.Projection("EPSG:900913")
                , units: "m"
                , maxResolution: 156543.0339
                , maxExtent: new OpenLayers.Bounds(-20037508.34, -20037508.34, 20037508.34, 20037508.34)
            };
        }
		//props.map.mapObject = new OpenLayers.Map( props.map.container , options );
        window[props.map.mapObjectName]= new OpenLayers.Map( props.map.container , options );
        props.map.mapObject = window[props.map.mapObjectName];
		//window[props.map.mapObjectName] = props.map.mapObject;
        //invoke the setBaseLayer function
        props.map.setBaseLayer.call(window, props);
		var cPt = new OpenLayers.LonLat(props.map.defaultCenter.lng, props.map.defaultCenter.lat);
		//cPt.transform(new OpenLayers.Projection("EPSG:4326"), props.map.mapObject.getProjectionObject());
        //props.map.mapObject.setCenter(cPt, props.map.zoom);
        window[props.map.mapObjectName].setCenter(cPt, props.map.zoom);

    }
    function plSetMultipleBaseLayers(props)
	{
        props.map.mapObject.addControl(new OpenLayers.Control.LayerSwitcher());
		if ( 1 == pl_properties.baseLayers["google"] && plSetGoogleBaseLayer) plSetGoogleBaseLayer(props);
        if ( 1 == pl_properties.baseLayers["ve"] && plSetVirtualEarthBaseLayer ) plSetVirtualEarthBaseLayer(props);
        if ( 1 == pl_properties.baseLayers["yahoo"] && plSetYahooBaseLayer ) plSetYahooBaseLayer(props);
        if ( 1 == pl_properties.baseLayers["wms"] && plSetWMSBaseLayer ) plSetWMSBaseLayer(props);
		
	}
	

    function plReproject ( coll, fromSrid, toSrid ) // coll is a OL feature collection. NOTE this modifies the colleciton not a clone
    {
        //transform the coordinates
        var oldProj = new OpenLayers.Projection("EPSG:" + fromSrid); //"EPSG:4326"
        var newProj = new OpenLayers.Projection("EPSG:" + toSrid);
		var reprojected = [];
        for (var i = 0; i < coll.length; i++)
        {
			coll[i].geometry.transform ( oldProj, newProj ) ;
        }
        return coll;
    }
    function plReprojectGeometry ( geom, fromSrid, toSrid ) // geom is an OL feature.geometry object
    {
        //transform the coordinates
        var oldProj = new OpenLayers.Projection("EPSG:" + fromSrid); //"EPSG:4326"
        var newProj = new OpenLayers.Projection("EPSG:" + toSrid);
		var g =  geom.clone();
		g.transform ( oldProj, newProj ) ;
        return g;
    }

    function plReprojectPoint( pt, fromSrid, toSrid ) // pt is NOT GEOJSON, it's {lng:?,lat:?}
    {
        //transform the coordinates
        //var proj = new OpenLayers.Projection();
        var pt = new OpenLayers.Geometry.Point(pt.lng, pt.lat);
        var fromProj = new OpenLayers.Projection("EPSG:" + fromSrid); //"EPSG:4326"
        var toProj = new OpenLayers.Projection("EPSG:" + toSrid);
        var pt2 = OpenLayers.Projection.transform(pt, fromProj, toProj);
        return { "lng" : pt2.x, "lat": pt2.y }
    }
function plGeoJson2OpenLayers( geojsonColl, type) // expects fature collection, all of same type
	{
		var str = JSON.stringify(geojsonColl);		
        var geojson_format = new OpenLayers.Format.GeoJSON();
		var out = geojson_format.read(str, type); // read can only take a string!
		return out;

	}
	
	function plOpenLayers2GeoJson ( geometry, pcode )
	{
		var geometry_proj;
		if ( pcode != pl_properties.geometrySrid ) 
		{
			geometry_proj = plReprojectGeometry (geometry, pcode, pl_properties.geometrySrid);
		}
		else
			{
			geometry_proj = geometry;
			}
		var olGeoJson = new OpenLayers.Format.GeoJSON();
		var geomGeoJson = olGeoJson.write ( geometry_proj ) ; // make it geojson, as a string
		var olJson = new OpenLayers.Format.JSON ();
		var geom = olJson.read (geomGeoJson);
		return geom;
	
		
	}
	function plMapAddMainLayer ( fColl, props, destroy ) // This takes GEOJSON for fcoll
	{
		plDebug("plMapAddMainLayer");
		//debugger;
		props.map.properties.styleMap = plGetMainMarkerStyleMap(); // TODO this should be based on a property in pl_properties
		plMapAddGeoJsonLayer ( fColl, pl_properties.geometrySrid, props, destroy );
	}

	function plMapAddGeoJsonLayer ( geoJson, projcode, props, destroy ) // assumes geoJson is a FeatureCollection
	{
		var ol = plGeoJson2OpenLayers ( geoJson );
		if ( projcode != 900913 ) plReproject (ol, projcode, 900913);
		// TODO handle geometry as needed
		plMapAddLayer( ol, props, destroy );
	}


    //displays Map using OpenLayers
	//if destroy is true (or null), remove existing features in that layer. if false, keep them.
	// fcoll must be an openlayers feature collection, NOT geojson.
    function plMapAddLayer ( fcoll, props, destroy )
    {
		if (fcoll.features && fcoll.type ) 
		{
			plDebug("you tried to pass GEOJSON to plMapAddLayer, first we will convert it.");
			plMapAddGeoJsonLayer ( fcoll, pl_properties.geometrySrid, props, destroy );
			return;
		}
        if ( !props )
        	{
        		var props = pl_properties.defaultRunProperties;
        	}
        if ( ! props.map || ! props.map.enabled ) {
        	plDebug("No map props!!!","warn")
        	return;
        }
        plDebug("in plMapAddLayer");
        if ( !window[props.map.mapObjectName] )
        {
            plLoadBaseMap(props);
        }
		else props.map.mapObject = window[props.map.mapObjectName];

		var layerName = ( props.map.layerName ? props.map.layerName : "default layer");
		var layer = props.map.mapObject.getLayersByName(layerName)[0];
        if ( layer )
        {
			if (destroy != false)
			{
				plDebug("destroying layer");
				//layer.destroy(); // destroy won't let you add it back again, says "this.div" is null, also may quip about this.event.
    	        layer.destroyFeatures();
			}
        }
		else
		{ 
		plDebug("props.map.properties=" + props.map.properties );
			layer = new OpenLayers.Layer.Vector( layerName, (  props.map.properties ? props.map.properties : { "isBaseLayer": false } ));
			props.map.mapObject.addLayer(layer);
			if ( props.map.onSelectFeatureOnMap)
			{
				var cb = {};
				if ( props.map.onMouseoverFeatureOnMap ) cb.over = props.map.onMouseoverFeatureOnMap;
				/*
				else cb.over = function (feature) { 
					if ( ct_mapPlacename ) ct_mapPlacename.destroy();
					
					ct_mapPlacename = new OpenLayers.Popup("ct_placename_popup",
								   feature.geometry.getBounds().getCenterLonLat(),
								   new OpenLayers.Size(200,40),
								   feature.attributes.name,
								   true);
					pl_map.addPopup(ct_mapPlacename);				
				}
				*/
				var selectControl = new OpenLayers.Control.SelectFeature(layer,
						{   onSelect: props.map.onSelectFeatureOnMap
							, onUnselect: props.map.onUnselectFeatureOnMap
							, hover: false // don't select on hover; false is the default.
							, callbacks : cb
						});
				props.map.mapObject.addControl(selectControl);
		        selectControl.activate();
			}
			
			
		}
		
		
		


		//add features to map
		if ( !props.map.style ) // if there is no custom style, use default style for the layer
		{
			plDebug("____ there was no style -_____");
			if (fcoll) layer.addFeatures( fcoll );
		}
		else
		{
			plDebug("____ style defined _____");
			for (var i in fcoll )
			{
				var feat = fcoll [i];
				feat.style = OpenLayers.Util.extend ( feat.style, props.map.style ) ;
				layer.addFeatures( [ feat ] );
			}
		}
		if (layer.visibility) // only do the zoom if visible
		{
			if (  props.map.zoomToLayer && layer.getDataExtent() ) 
			{
				plDebug("zooming to layer extent");
				props.map.mapObject.zoomToExtent ( layer.getDataExtent() );
			
			}
			else if ( props.map.zoom > 0 ) 
			{
			plDebug("zooming to zoom level " + props.map.zoom)
			if ( props.map.defaultCenter && props.map.defaultCenter.lat &&props.map.defaultCenter.lng ) 
			{
				var cPt = new OpenLayers.LonLat(props.map.defaultCenter.lng, props.map.defaultCenter.lat);
				props.map.mapObject.setCenter( cPt, props.map.zoom)
			}
			else props.map.mapObject.zoomTo ( props.map.zoom );
			}
			
		}
    }

    //closes the Feature popup
    //TODO -- NOT WORKING -- because we need a handle to this map's SelectFeature Control
    function plOnPopupClose(evt)
    {
		plDebug("plOnPopupClose");
		this.hide();
        //if ( pl_selectedFeature) pl_selectControl.unselect(pl_selectedFeature);
    }



    function plCriteriaAdvanced()
    {
        alert ('Advanced Criteria - TODO -- choose spatial function, buffer,..');
    }

    function plFiltersAdvanced()
    {
        alert ('Advanced Filters: TODO -- all main attributes');
    }



    //clear the plEleError element
    function plClearError() // TODO we use a dialog now so hide it (see next function plErrorHandler
    {
        var d = plById("plEleError");
        if (d)
        {
            d.innerHTML = "";
        }
    }

    //shows error in the plEleError element
    function plErrorHandler(ty, msg)
    {
        if ( plShowProcessing) plShowProcessing(false);
		/*
        var d = plById("plEleError");
        if (d)
        {
            d.innerHTML = ty + " " + msg;
        }
		*/
		var details = msg ? msg : "There were no details on this error.";
		if ( plShowPanel) plShowPanel ("plEleError", ty, details );
		else alert(ty +": " + details);
    }


    function plTrackBehavior(callid, xml)
    {
        plDebug("track behavior xml=" + xml);
        plCookie( pl_TRACK_BEHAVIOR_COOKIE_PREFIX + callid, xml, { expiry: (30*6) }); 
    }

    function plSaveResultItem(fsId, fId, extId, mlsid, name) //TODO need to be able to save other attributes as needed but keep the size small
    {
        var t = (new Date()).getTime();
        plCookie(pl_SAVED_ITEM_COOKIE_PREFIX + t, fsId +"~~" + fId + "~~" + extId + "~~" + mlsid + "~~" + name, { expiry: (30*6) });
    }

    function plContactForm(id)
    {
        var cid = (id ? id : "plEleContact");
        if ( plById(cid))
        {
			//(eleId, title, bodyHtml, props, beforeHideFunction )
			//
            plShowDialog( cid, "Contact an agent"); // ct_showPanel (eleId, title, bodyHtml, props, beforeHideFunction )
        }
        else
        {
            alert ('TODO');
        }
    }

    function plParseContactForm(formId)
    {
        var fid = (formId ? formId : "plEleContactForm");
        var contactInfo = {};
        var form = plById(fid);
        if ( form )
        {
            contactInfo = plFormToMap(form);
            plContactSend(contactInfo);
        }
    }

    function plContactSend(contactInfo)
    {
		plShowProcessing (true);
		var content = "contact=";
        for ( var k in contactInfo)
        {
            content += "~~~" + k + "~~" + escape(contactInfo[k]);
        }
        content += "&hx=";
        //get all behavior cookies
        //var hx = plCookie("(" + pl_TRACK_BEHAVIOR_COOKIE_PREFIX + "[0-9]*)");
        var hx = plCookie(pl_TRACK_BEHAVIOR_COOKIE_PREFIX);
        //plDebug(document.cookie);
        if ( hx )
        {
            for ( var i=0; i< hx.length; i++)
            {
                content += "~~~" + "hx_" + i + "~~" + escape(hx[i]);
            }
            //content += "~~~" + "hx_" + i + "~~" + escape(hx);
        }
        //get all saved items cookies
        content += "&saved=";
        //var s = plCookie("(" + pl_SAVED_ITEM_COOKIE_PREFIX + "[0-9]*)");
        var s = plCookie(pl_SAVED_ITEM_COOKIE_PREFIX);
        if ( s )
        {
            for ( var i=0; i< s.length; i++)
            {
                content += "~~~~" + "saved_" + i + "~~~" + escape(s[i]);
            }
        }
        plDebug("content=" + content);
        var callid = plGetNewCallId();
		alert("need to fix plcallxdomian");
		
        //plCallXDomain (callid, pl_properties["servicesUrlBase"] + "/contact/json/submit.action?version=1.0&apikey=" + pl_properties["apiKey"] + "&callback=plContactCallback&callid=" + callid +"&" + content);
    }

    function plContactCallback(data, callid)
    {
		plShowProcessing(false); 
        if ( data && data.response && data.response.status && 'SUCCESS' == data.response.status.code)
        {
            alert ('Your contact information has been sent.');
            var cid = (id ? id : "plEleContact");
            if ( plById(cid))
            {

            }
        }
        else
        {
            plErrorHandler("Contact agent failed", data.response.status.message );
        }
    }


	// shows all weighted criteria on map
	function plFindNearbyCriteria ( geom )
	{
		//alert(geom);
		plDebug("in plFindNearbyCriteria");
		var layers = pl_map.getLayersByName("Criteria");
		if (layers && layers[0] && layers[0].destroyFeatures) layers[0].destroyFeatures();
		for ( var i=0; i< pl_criteria.length; i++ )
		{
            plDebug("formula pos=" + pl_criteria[i].formulaPosition );
			if ( pl_criteria[i].formulaPosition > -1 )
			{
				var params="";
				for ( var j in pl_criteria[i].params )
				{
					params += j + "=" + pl_criteria[i].params[j] + "&";
				}
				var buffer = pl_criteria[i].defaults.nearbyBuffer;
				plCallNearbyService ( "plCallNearbyServiceCallback", geom, "geojson", buffer, "mile", pl_criteria[i].fsid, params, "distance", 5, 0, { criteriaIndex : i } )
			}
		}
	}

    function plCallNearbyService ( callbackName, geom, geomFormat, buffer, bufferUnits, criteriaId, params, order, limit, offset, extra, transportString )
    {
		plDebug("in plCallNearbyService");
		//debugger;
		//bufferUnits not used? criteriID not used maybe in callback? params? extra? transportString?
		//order = distance | id | name
		// limit should default to 5, offset to 0
        //var cid = plGetNewCallId() ;
        //var common = 'version=1.0&callid=' + cid + '&callback=' + callbackName  +'&apikey=' + pl_properties.apiKey;
		//var url = pl_properties["servicesUrlBase"] + "/nearby/json/run.action?limit=" + limit + "&offset=" + offset + "&order=" + order + "&geometry=" + escape(geom) + "&geomFormat=" + geomFormat + "&buffer=" + buffer + "&bufferunits=" + bufferUnits + "&criteria=" + criteriaId +  "&" + params + "&" + common;
        //plCallXDomain ( cid, url, extra, transportString );
		var args = {
			"callback":callbackName
			//,"uid":null
			,"geometry": geom
			,"geometryformat": geomFormat
			,"order":order
			,"buffer":buffer
			,"criteria":criteriaId
			,"limit":limit
			,"offset":offset
			,"PARAMETERS":params //TODO is params above formatted right?
			,"extra":extra
			}
		PL.restapi.nearby.run(args);
	    }
	
	function plCallNearbyServiceCallback ( data, callid )
	{
		plDebug("plCallNearbyServiceCallback");
		//debugger;
		if ( data && data.response && data.response.status && 'SUCCESS'== data.response.status.code )
        {
            //call succeeded.
			var props = pl_callprops["call_" + callid];
			var featureCollection = data.response.content;
			plShowNearbyOnMap ( featureCollection, props.extra.criteriaIndex )
        }
        else
        {
            plErrorHandler("Find nearby failed", data.response.status.message);
		}
	}
	
	function plShowNearbyOnMap ( featureCollection, criteriaIndex )
	{
		var defaultProps = pl_properties.defaultRunProperties;
		var props = {
			map: {  enabled : true
				, container: defaultProps.map.container
				, layerName: "Criteria"
				, mapObjectName : defaultProps.map.mapObjectName
				, onSelectFeatureOnMap : defaultProps.map.onSelectFeatureOnMap
				, onUnselectFeatureOnMap : defaultProps.map.onUnselectFeatureOnMap
				, style : plGetNearbyMarkerStyle ( pl_criteria[criteriaIndex].defaults.marker )
			}
		}
		plMapAddGeoJsonLayer ( featureCollection, pl_properties.geometrySrid, props, false )
		
	}
	function plGetMainMarkerStyleMap ()
	{
		plDebug("this is the default plGetMainMarkerStyleMap");
		return new OpenLayers.StyleMap(new OpenLayers.Style(
				{
					"graphicWidth" : 17
					, "graphicHeight" : 19
					, "externalGraphic":"${getGraphicURL}"
				}
				, {
					"context" : 
						{
							"getGraphicURL": function(feature)
								{
									var rank = feature.attributes.rank?feature.attributes.rank:"";
									//return "../../../res/images/mapmarkers/lightblue/lightblue" + rank + ".png";
                                    return pl_properties["servicesUrlBase"] + "/../res/images/mapmarkers/lightblue/lightblue" + rank + ".png";
								}
					}
				})
			)
	}
	function getNearbyMarkerStyle (m) {return plGetNearbyMarkerStyle(m)}//legacy
	function plGetNearbyMarkerStyle ( markerName )
	{
		plDebug("getNearbyStyle, markerName="+ markerName );
		var markerPath = pl_properties["servicesUrlBase"] + "/../res/images/mapmarkers/";
		var altPath = pl_properties["servicesUrlBase"] + "/../res/3rdParty/images/mapmarkers/dkarr/"; // only some of them use this		
		//if ( ! markerName ) var markerName = "default";
		var styles = {
				"default" : { 
					"graphicWidth" : 21
					, "graphicHeight" : 31
					, "strokeColor" : "#FF0000"
					, "strokeWidth" : 3
					, "fillOpacity": 0
					, "fillColor" : "#FF0000"
					, "strokeWidth":1
					, "strokeColor":"#FF0000"
					, "strokeOpacity": 1
					}
				,
			"defaultPoint" : { 
				"graphicWidth" : 17
				, "graphicHeight" : 19
				, "graphicXOffset" : -8
				, "graphicYOffset" : -19
				//, "externalGraphic" : "../../../res/images/mapmarkers/criteria/yellow.png"
                , "externalGraphic" : markerPath + "lightblue/lightblue.png"
				}
			, "defaultLine" : { 
				"strokeColor" : "#FF0000"
				, "strokeWidth" : 3
				}
			, "defaultPolygon" : { 
				 "fillOpacity": 0.4
				, "fillColor" : "#FF0000"
				, "strokeWidth":1
				, "strokeColor":"#FF0000"
				, "strokeOpacity": 1
				}
			, "golf" : { 
				"graphicWidth" : 21
				, "graphicHeight" : 31
				, "graphicXOffset" : -10
				, "graphicYOffset" : -31
				, "externalGraphic" : altPath+"golf.png"
				, "fillOpacity": "1"
				}
			, "yellow" : { 
				"graphicWidth" : 21
				, "graphicHeight" : 31
				, "graphicXOffset" : -10
				, "graphicYOffset" : -31
				, "externalGraphic" : markerPath+"criteria/yellow.png"
				, "fillOpacity": "1"
				}
			, "hospital" : { 
				"graphicWidth" : 21
				, "graphicHeight" : 31
				, "graphicXOffset" : -10
				, "graphicYOffset" : -31
				, "externalGraphic" : altPath+"hospital.png"
				, "fillOpacity": "1"
				}
			, "lightrail" : { 
				"graphicWidth" : 21
				, "graphicHeight" : 31
				, "graphicXOffset" : -10
				, "graphicYOffset" : -31
				, "externalGraphic" : altPath+"lightrail.png"
				, "fillOpacity": "1"
				}
			, "crime" : {
				 "fillOpacity": 0.4
				, "fillColor" : "#FF0000"
				, "strokeWidth":1
				, "strokeColor":"#FF0000"
				, "strokeOpacity": 1
				}
			, "tiny_red" : {
				"graphicWidth" : 9
				, "graphicHeight" : 12
				, "graphicXOffset": -5
				, "graphicYOffset": -12
				, "externalGraphic" : markerPath+"criteria/tiny_red.png"
				, "fillOpacity": "1"
				}
			, "tiny_red_ast" : {
				"graphicWidth" : 9
				, "graphicHeight" : 12
				, "graphicXOffset": -5
				, "graphicYOffset": -12
				, "externalGraphic" : markerPath+"criteria/tiny_red_ast.png"
				, "fillOpacity": "1"
				}
			};
		var style = styles[markerName] ? styles[markerName] : styles["defaultPoint"];
		
			plDebug("getNearbyMarkerStyle, style=" + style);
/*
var style = new OpenLayers.Style({
"graphicWidth" : 17
, "graphicHeight" : 19
, "externalGraphic":"../../../res/images/mapmarkers/criteria/marker_2b.png"
});
*/
		return style;
	}

    function plRenderFeaturesAsBlog(resp, callid)
    {
        //plDebug(resp);
        var props = plGetCallProperties(callid);
		//plDebug(JSON.stringify(props));
        var canedit   = (PLAdmin?PLAdmin.hasEditPrivileges():false); //If PLAdmin module is accessible for customer and user has edit privs
        var candelete = (PLAdmin?PLAdmin.hasDeletePrivileges():false); //If PLAdmin module is accessible for customer and user has edit privs
        var html ='';
        var tpl = props["template"];
        var container = plCoalesce(props["container"], "resultsContainer");
        if ( plIsEmpty(tpl))
        {
            //default template
            tpl ='<div id="entry${index}" class="row ${oddOrEven}">'
                    + '<div class="hd">'
                    + '<img src=""></img><a href="#">${userName}</a>'
                    + '</div>'
                    + '<div class="bd">${description}</div>'
                    + '<div class="ft">${edit}</div>'
                + '</div>';
        }
        var fsid = props["fsid"];
        for ( var i=0; i< resp.response.content.features.length; i++)
        {
            var f = resp.response.content.features[i];
            var edit = "";
            if (true == canedit) edit = '<a class="edit" href="javascript:PLAdmin.showEditFeaturePanel(\'blog\',' + fsid +',' + f.id +')">edit</a> ';
            if (true == candelete) edit += '<a class="edit" href="javascript:PLAdmin.showDeleteFeaturePanel(\'blog\',' + fsid +',' + f.id +')">delete</a>';

            var data = { index: i, oddOrEven: (i%2==0?"even":"odd"), userName: "todo", "fsid": fsid, "id": f.id, "edit": edit };
            
		for ( var p in f.properties)
			{
				if (props.formatters && props.formatters[p])
				{
					data[p] = props.formatters[p].call ( this, f.properties[p] );
				}
				else data[p] = f.properties[p];

				//else plDebug("no formatter for " + p );
			}
            html += plMergeTemplate(tpl, data);
        }
		if (i == 0) html += '<div>There are no matching results.</div>';
        html += (canedit?'<div style="clear: both"><a class="add" href="javascript:PLAdmin.showAddFeaturePanel(\'Event\', ' + fsid +')">Add</a></div>':"");
		if ( !plIsNull(props["nextFunctionName"]) && !plIsNull(props["offset"]) && !plIsNull(props["limit"])) 
		{
	        var offsetForward = props["offset"] + props["limit"];
	        var offsetBack = props["offset"] - props["limit"];
			html += '<div style="clear: both"><a href="javascript:void(0)" onClick="' + props["nextFunctionName"] + '(' + offsetBack + ')">&lt;&lt; Back</a> | <a href="javascript:void(0)" onClick="' + props["nextFunctionName"] + '(' + offsetForward + ')">Next &gt;&gt;</a></div>'; // TODO this need to be parameterized
		}
        plById(container).innerHTML = html; // TODO any invalid HTML kills IE!
        //plById("resultsFooter").innerHTML =
    }

    function plRenderFeaturesAsThread(resp, callid) //render features like a discussion thread
    {
        plDebug(resp);
        var canedit = (PLAdmin?PLAdmin.hasEditPrivileges():false); //If PLAdmin module is accessible for customer and user has edit privs
        var candelete = (PLAdmin?PLAdmin.hasDeletePrivileges():false); //If PLAdmin module is accessible for customer and user has edit privs
        var html ='';
           tpl ='<div id="entry${index}" class="entry ${oddOrEven}">'
                    + '<div class="hd">'
                    + '<img src=""></img><a href="#">${userName}</a>'
                    + '</div>'
                    + '<div class="bd">${description}</div>'
                    + '<div class="ft">${edit}</div>'
                + '</div>';
        var fsid = plGetCallProperties(callid)["fsid"];
        for ( var i=0; i< resp.response.content.features.length; i++)
        {
            var f = resp.response.content.features[i];
            var edit = "";
            if (true == canedit) edit = '<a class="editFeatureIcon" href="javascript:PLAdmin.showEditFeaturePanel(\'thread\',' + fsid +',' + f.id +')">reply</a> ';
            if (true == candelete) edit += '<a class="deleteFeatureIcon" href="javascript:PLAdmin.showDeleteFeaturePanel(\'thread\',' + fsid +',' + f.id +')">delete</a>';

            html += plMergeTemplate(tpl, { index: i, oddOrEven: (i%2==0?"even":"odd"), userName: "todo"
                    , description: f.properties.description, fsid: fsid, id: f.id
                    , "edit": edit
                });
        }
        plById("resultsContainer").innerHTML += html;
        plById("resultsFooter").innerHTML += (canedit?'<a class="add" href="javascript:PLAdmin.showAddFeaturePanel(\'thread\', ' + fsid +')">Add</a>':"");
    }
/***  end of Formula stuff **/

            /***  PLAdmin stuff **/
////dependencies: util.js
//              pl_properties
//plDebug("loading admin stuff");
var pl_rest_version;
function PLAdminShowEditDone (a,b) { // can't figure out how to get the callback to work if this is a child of PLadmin
//Stub ... overwrite with function to resize and/or recenter
							  }
var pl_admin_location_map = null; // I had to keep this out of PLAdmin, else the plLoadBaseMap() was not working with PLAdmin.featureMap
var PLAdmin = {
    //constants (in caps)
      DEFAULT_LIMIT : 10
    , MODE_ADD : 0
    , MODE_EDIT : 1
    , MODE_REPLY : 2
    , ATTRIBUTE_TYPES : {  "l": { "label": "list", "widget": "select", "pattern": "", "msg": "", "maxsize": "50"}
                        , "n": { "label": "number", "widget": "textbox", "pattern": "^([0-9]){0,}(\\.){0,1}([0-9]){0,}$", "msg": "invalid number", "maxsize": "20"}
                        , "c": { "label": "currency", "widget": "textbox", "pattern": "^(\\$){0,1}([0-9]){0,}(\\.){0,1}([0-9]){0,}(\\$){0,1}$", "msg": "invalid currency", "maxsize": "20"}
                        , "i": { "label": "integer", "widget": "textbox", "pattern": "^([0-9]){0,}$", "msg": "invalid number", "maxsize": "10"}
                        , "d": { "label": "date", "widget": "textbox", "pattern": "^([0-9]){2,4}-([0-9]){2,2}-([0-9]){2,2}(\\s[0-9][0-9]:[0-9][0-9]:[0-9][0-9]){0,1}$", "msg": "invalid date (yyyy-mm-dd)", "maxsize": "10"}
                        , "b": { "label": "double", "widget": "textbox", "pattern": "^([0-9]){0,}(\\.){0,1}([0-9]){0,}$", "msg": "invalid number", "maxsize": "20"} //same as number
                        , "s": { "label": "string", "widget": "textbox", "pattern": "", "msg": "", "maxsize": "250"}
                        , "t": { "label": "text", "widget": "textarea", "pattern": "", "msg": "", "maxsize": "10000"}
                        , "e": { "label": "editor", "widget": "texteditor", "pattern": "", "msg": "", "maxsize": "10000"}
                        , "p": { "label": "image url", "widget": "textbox", "pattern": "^http(s){0,1}://[.]*$", "msg": "invalid url", "maxsize": "100"}
                        , "u": { "label": "URL", "widget": "textbox", "pattern": "^http(s){0,1}://[.]*$", "msg": "invalid url"}
                        , "v": { "label": "video URL", "widget": "textbox", "pattern": "^http(s){0,1}://[.]*$", "msg": "invalid url"}
                        , "h": { "label": "hidden", "widget": "hidden", "pattern": "", "msg": ""}
                        , "g": { "label": "location", "widget": "location", "pattern": "", "msg": ""}
                        , "m": { "label": "email", "widget": "textbox", "pattern": "^(\\w+)@(\\w+\\.)(\\w+)(\\.\\w+)*$", "msg": "invalid email"}
    }
    , REQUIRED : true
    , OPTIONAL : false
    , LOCATION_ADDRESS_DEPENDENTS : ["_location_buffer", "_location_buffer_units"] //TODO add the DONE button
    , UNITS : [   {"code": "mile", "label": "miles", "factor": { "4326": (1/69.1), "900913": 0.0006213712 } } // note factor is
                , {"code": "feet", "label": "feet", "factor": {"4326": (0.000189394/69.1), "900913": 0.000189394 } }
                , {"code": "km", "label": "KM", "factor": { "4326": (0.6213712 / 69.1), "900913": 0.6213712 } }
                , {"code": "m", "label": "meters", "factor": {"4326": (0.0006213712 / 69.1), "900913": 0.0006213712 } }
                //, {"code": "block", "label": "blocks", "factor": { "4326": 9.1, "900913":0}  }
            ]
    //variables
    , fsCache : {}
    , currentFSId : "" // current feature set
    , currentFId : ""
    , featureMap : null // map object
    , featureSaveSuspended : false // whether the "save" feature action is suspended, until the location address geocode callback is received.
    , fe_widgets : {} //each entry in this array will have the structure { "name": "", "type": "simpleeditor", "handle": ref}
    , _geoJsonFormatter : null
    //
    , getGeoJsonFormatter: function()
    {
        if ( ! PLAdmin._geoJsonFormatter) PLAdmin._geoJsonFormatter = new OpenLayers.Format.GeoJSON()
        return PLAdmin._geoJsonFormatter;
    }
    //gets the PL profile of the user who's logged in. Mainly the uid. Thereafter all admin calls must send the uid
    ,getPlacelogicUserProfile: function(callbackName, user, props)
    {
        var callid = plGetNewCallId();
        var url = pl_properties["servicesUrlBase"] + "/admin/json/user.action?"
            + "externalid=" + user.externalId + "&name=" + user.profile.name
            + "&groups=" + (user.profile.groups.items?user.profile.groups.items.join(","):"")
            + "&networks=" + (user.profile.networks.items?user.profile.networks.items.join(","):"")
            + "&callid=" + callid + "&version=1.0&apikey=" + pl_properties.apiKey
            + "&callback=" + callbackName;
        plDebug("url=" + url);
        plCallXDomain(callid, url, props);
    }
    , isFeatureLocationAddressDirty: function()
    {

    }
    , hasEditPrivileges : function()
    {
        //assumes we only query the admin group
        if ( pl_User.id > 0 && pl_User.profile.groups.items.length> 0 )
        {
            return true;
        }
        else return false;
    }
    , hasDeletePrivileges : function(fsid)
    {
        //TODO - load meta on page load, so that we can check if the user is the owner/co-owner
        //assumes we only query the admin group
        if ( pl_User.id > 0 && pl_User.profile.groups.items.length> 0 )
        {
            return true;
        }
        else return false;
    }
    ,
    //lists the Features for a given fsid
    listFeatures : function(callbackName, profile, fsid, paramsHArray, limit, offset, props )
    {
        alert ('TODO');
    }
    ,
    //gets a Feature for a given fsid + fid
    getFeature : function (callbackName, uid, fsid, fid, props)
    {
        var callid = plGetNewCallId();
        var url = pl_properties["servicesUrlBase"] +  '/admin/json/getFeature.action?fsid=' + fsid + '&fid=' + fid + '&uid=' + uid
                        + "&callid=" + callid + "&version=1.0&apikey=" + pl_properties.apiKey
                        + "&callback=" + callbackName;
        plCallXDomain(callid, url, props);
    }
    ,
    //adds a Feature into the FeatureSet
    addFeature : function( args ) // callback, uid, attributes
    {
		
		plDebug('PLAdmin.addFeature');
		//plDebug(params);
		//var p = { uid: uid, callback: callbackName, name: params.name, description: params.description, geometry: params.geometry, fsid: params.fsid , params: params } 
		var p = args.attributes;
		p.callback = args.callback;
		p.uid=args.uid;
		PL.restapi.place.add ( p );
    }
    ,
    //edits a Feature
    editFeature : function( args )
    {
		plDebug('PLAdmin.editFeature');
		var p = args.attributes;
		p.callback = args.callback;
		p.uid=args.uid;
		PL.restapi.place.edit(p);
		/*
        plDebug('PLAdmin.editFeature');
        plDebug(params);
        var callid = plGetNewCallId();
        var url = pl_properties["servicesUrlBase"] +  '/admin/json/editFeature.action?uid=' + uid
                        + "&" + plToDelimtedText(params, "=", "&")
                        + "&callid=" + callid + "&version=1.0&apikey=" + pl_properties.apiKey
                        + "&callback=" + callbackName;
        plCallXDomain(callid, url, props);
		*/
    }
    ,
    //deletes a Feature
    deleteFeature : function(callbackName, uid, fsid, fid, props)
    {
		alert ('TODO');
    }
    //Feature Rendering



    //FEature Editor stuff
	, showPanelSetup: function ( title, saveFunction ) // onClickString is the complete functionanme for the onClick function including()
	{
		// (eleId, title, bodyHtml, props, beforeHideFunction )
		//var html;
		//if ( ! plById("plFeatureEditorPanelBody") )
		var bodyHtml = '<div id="plFeatureEditorPanelBody"></div>';// only write this first time around. 2009.09.26 plShowdialog destroys prior widget now so send html every time
        //var footerHtml = '<button onClick="'+onClickString+'">Save</button>';
		plShowDialog( 
					   "plFeatureEditorPanelEle"
					 , title
					 , bodyHtml
					 , {	width: "600px"
					 		, height: "500px"
							, modal: true
							, zIndex: 2000
							, buttons: { "Save" : saveFunction } // YUI dialog bug: if modal is true, the zIndex can't be set! But if there's already a map on the page, teh Zindex must be higher than default.
					 }
					 );
	}
    , showEditFeaturePanel: function(label, fsid, fid)
    {
		//var html = '<div id="plFeatureEditorPanelBody"></div>';
        //plById("plFeatureEditorPanelHead").innerHTML = "Add " + label;
		//var dialogprops = {width: "600px", height: "500px", modal: true, zIndex: 300 };
        //plShowDialog("plFeatureEditorPanelEle", "Edit " + label, html, dialogprops )//PLAdmin.onEditFeatureCancel,PLAdmin.onEditFeatureSave, false);//(eleId, title, bodyHtml, props, beforeHideFunction )
        //get the metadata
        PLAdmin.showPanelSetup ( "Edit " + label, PLAdmin.onEditFeatureSave );
		PLAdmin.startEdit ("PLAdminShowEditDone", "plFeatureEditorPanelBody", fsid, fid, "edit");
    }
    , showReplyFeaturePanel: function(label, fsid, fid)
    {
		var html = '<div id="plFeatureEditorPanelBody"></div>';
		var dialogprops = {width: "600px", height: "500px", modal: true, zIndex: 300 };
        plShowDialog("plFeatureEditorPanelEle", "Reply to " + label, html, dialogprops )//PLAdmin.onEditFeatureCancel,PLAdmin.onReplyFeatureSave, false);
        //get the metadata
        PLAdmin.startEdit ("plShowEditDone", "plFeatureEditorPanelBody", fsid, fid, "reply");
    }

    , showAddFeaturePanel: function(label, fsid)
    { 
		//var html = '<div id="plFeatureEditorPanelBody"></div>';
		//var dialogprops = {width: "600px", height: "500px", modal: true, zIndex: 300 };
        //plShowDialog("plFeatureEditorPanelEle", "Add " + label, html, dialogprops );//(eleId, title, bodyHtml, props, beforeHideFunction )
        //get the metadata
        PLAdmin.showPanelSetup ( "Add " + label, PLAdmin.onAddFeatureSave );
		var props = {"mode": "add", "fsid": fsid, "fid": 0, "onsuccess": "PLAdminShowEditDone", "container": "plFeatureEditorPanelBody"};
        if ( PLAdmin.fsCache[fsid+""])
        {
            //metadata is cached on browser, so use this.
            PLAdmin.renderWidgets(props.onsuccess, props.container, props.mode, PLAdmin.fsCache[fsid+""], fsid, null, {"geometry":{}, "properties":{}});
        }
        else
        {
            //get metadata from server
			props.callback="PLAdmin.metadataAddCallback";
			props.fsid = fsid;
			props.uid = pl_User.id;
            PLAdmin.getMetaData( props );
        }
    }


    //workflow: get metadata for FS
    //get feature
    //display widgets
    , startEdit:function(callbackName, elementId, fsid,fid, mode)
    {
        PLAdmin.currentFSId = fsid;
        PLAdmin.currentFId = fid;
        var props = {"mode": mode, "fsid": fsid , "fid": fid, "onsuccess": callbackName, "container": elementId};
        if ( PLAdmin.fsCache[fsid+""])
        {
            //metadata is cached on browser, so use this. Now get the feature. Always get from server, so its uptodate.
            PLAdmin.getFeature("PLAdmin.getEditFeatureCallback", pl_User.id, fsid, fid, props );
        }
        else
        {
            //get metadata from server
			props.callback="PLAdmin.metadataCallback";
			props.fsid = fsid;
            PLAdmin.getMetaData( props );
        }
    }
    //gets the FeatureSet metadata for a given fsid
	,
    getMetaData: function ( props )
    {
			PL.restapi.attribute.list ( props );
    }
    , metadataCallback : function(d, callid)
    {
        plDebug("PLAdmin.metadataCallback");
        if ( d.response.status.code == 'SUCCESS')
        {
            var props = plGetCallProperties(callid);
            var fsid = props["fsid"];
			PLAdmin.fsCache[fsid] = d.response.content.items ; //put FS in cache
            PLAdmin.getFeature("PLAdmin.getEditFeatureCallback", pl_User.id, props.fsid, props.fid, props );
        }
        else
        {
            alert ("PLAdmin.metadataCallback::error " + d.response.status.message);
        }
    }
    , metadataAddCallback: function(d, callid) // TODO maybe merge this with metadataCallback
    {
		//debugger;
		plDebug("PLAdmin.metadataAddCallback");
        if ( d.response.status.code == 'SUCCESS')
        {
			var props = plGetCallProperties(callid);
			var fsid= props["fsid"]+"";
			PLAdmin.fsCache[fsid] = d.response.content.items ; //put FS in cache
			PLAdmin.renderWidgets(props.onsuccess, props.container, props.mode, PLAdmin.fsCache[fsid+""], fsid, null, {"geometry":{}, "properties":{}});
        }
        else
        {
            alert ("PLAdmin.metadataAddCallback::error " + d.response.status.message);
        }
    }
    , getEditFeatureCallback : function( d, callid)
    {
        plDebug("PLAdmin.getEditFeatureCallback");
        if ( d.response.status.code == 'SUCCESS')
        {
            var props = plGetCallProperties(callid);
            var f = d.response.content.features[0];
			var fsid = f.properties.featureSetId;
			var fid = f.id;
            plDebug("got feature");
            plDebug(d);
            PLAdmin.renderWidgets(props.callback, "plFeatureEditorPanelBody", props.mode, PLAdmin.fsCache[props.fsid+""], fsid, fid, f);
        }
        else
        {
            alert ("PLadmin.getEditFeatureCallback::error " + d.response.status.message);
        }
    }
//Feature editor widgets
    , popupContainerId : null
    , cleanUpWidgets : function(eleid)
    {
        PLAdmin.popupContainerId = eleid;
        PLAdmin.destroyWidgets();

        //TODO create Location as a widget
        var container = plById(eleid);
        plRemoveChildrenFromNode(container);
        PLAdmin.fe_widgets = []; //TODO remove the entries one by one before resetting
    }
    , renderWidgets:function(callbackName, eleid, mode, attrMetaData, fsid, fid, feature)
    {
        plDebug("plRenderWidgets mode=" + mode );
        PLAdmin.cleanUpWidgets(eleid);
        if ( (mode == 'edit' || mode=='reply')&& (plIsObjectEmpty(feature) || feature.id == "") )
            alert ( 'Feature unavailble for ' + mode);
        //plDebug(mode=='edit'&& feature?feature.properties["name"]:"NO_DATA!!");
        var container = plById(eleid);
        var locattrs = { "label": "Location", "type": "g", "locationMode": "edit", "location": "", "value": "" };
        if ( mode == 'edit' || mode == 'reply')
        {
                locattrs["locationMode"]="view";
                locattrs["location"]= feature["properties"]["_5flocationscheme"];
                locattrs["value"] = JSON.stringify(feature["geometry"]);
        }

        //now create the widgets
		//debugger;
        PLAdmin.createWidget("fsid", "hidden", container, "fsid", fsid);
        PLAdmin.createWidget("fid", "hidden", container, "fid", fid);
        PLAdmin.createFormField("geometry", container, mode, locattrs);
        PLAdmin.createFormField("name", container, mode, {"name":"name", "type": "s", "label": "Title", "value": ((mode=='edit'||mode=='reply')&& feature?feature.properties["name"]:"")});
        if ( mode == "reply" && feature ) //show the parent thread content )
        {
            plAddHtmlNode(container, "div", feature.properties["description"], {"class": "parentEntryDescription"});
        }
        PLAdmin.createFormField("description", container, mode, {"name": "description", "type": "e", "label": "Content", "value": (mode=='edit'&& feature?feature.properties["description"]:"")});
        //now other attributes.
        for ( var aname in attrMetaData )
        {
            var a = attrMetaData[aname];
            var v = feature.properties[a.name];
            plDebug("Attribute " + aname + "(aka " + a.name + ")="   + v);
			if ( a.name == "_owneruid" && !v ) v=pl_User.id; // this is a special field to duplicate the user_id so we can query it as an attribute
            if ( a.type != 'g') // ignore the _location "hidden" attribute
            {
                PLAdmin.createFormField(a.name, container, mode, a, v);
            }
        }
        if (! plIsEmpty(callbackName))
        {
            var f = new Function(callbackName);
            if ( f ) f.apply();
        }

    }
    , onEditFeatureSave: function()
    {
        return (PLAdmin._onFeatureSave('edit'));
    }
    , onAddFeatureSave: function()
    {
        return (PLAdmin._onFeatureSave('add'));
    }
    , onReplyFeatureSave: function()
    {
        return (PLAdmin._onFeatureSave('reply'));
    }
    //private method that handles the saving of a feature
    //mode - edit,add,reply
    , _onFeatureSave: function(mode)
    {
        plDebug("in _onFeatureSave, mode="+mode);
        try
        {
			//debugger;
            var data = PLAdmin.getInputData(true); //true --clear Error messages
			var errs = PLAdmin.validateFeature(mode, data);
            if ( plIsObjectEmpty(errs))
            {
                if ( mode == 'edit')
                {
                    PLAdmin.editFeature( { callback: "PLAdmin.editFeatureCallback", uid: pl_User.id, attributes : data } );
                }
                else if ( mode == 'add' || mode == 'reply')
                {
                    PLAdmin.addFeature( { callback: "PLAdmin.editFeatureCallback", uid: pl_User.id, attributes : data } );
                }
                else
                {
                    alert ('PLAdmin._onFeatureSave: unhandled mode:' + mode);
                }
            }
            else
            {
                alert ("Validation error: please review the form and correct any flagged fields.");
                return false;
            }
            //PLAdmin.cleanUpWidgets(PLAdmin.popupContainerId);
            return true;
        }
        catch (e)
        {
           alert ("onEditFeatureSave::Error: " +  e.message);
           return false;
        }
    }
    //when the canacel button is clicked on the feature editor
    , onEditFeatureCancel: function()
    {
        PLAdmin.cleanUpWidgets(PLAdmin.popupContainerId);
        return true;
    }
    , editFeatureCallback: function(data, callid)
    {
        plDebug("editFeatureCallback");
		if (data && data.response && data.response.status )
		{
			if ( data.response.status.code == "SUCCESS")
			{
				PLAdmin.cleanUpWidgets(PLAdmin.popupContainerId);
				plHideDialog("plFeatureEditorPanelEle");
			}
			else 
			{
				
				alert("Server responded: "+ data.response.status.message )
			}
		}
		else plDebug("Reponse was not success but there was no message returned", "warn");
    }
    //validates the feature data based on the defined rules and meta data
    , validateFeature: function(mode, data)
    {
        plDebug("PLAdmin.validateFeature mode="+mode +" data=" + data);
        plDebug("fsid=" + data.fsid);
        var errs = {};
        if ( plIsEmpty(data.fsid)) errs["fsid"] = "FeatureSet id is missing";
        if (( mode == 'reply' || mode == 'edit') && plIsEmpty(data.fid) ) errs["fid"] = "Feature Id is missing";

        PLAdmin.validate(errs, "name", data["name"], PLAdmin.REQUIRED, 0, 300, "^[a-zA-Z0-9\\s\\.\\-!]*$", "only alpha numerals . - ! _ are allowed");
        PLAdmin.validate(errs, "description", data["description"], PLAdmin.REQUIRED, 0, 10000);
        PLAdmin.validate(errs, "geometry", data["geometry"], PLAdmin.REQUIRED, 0, 10000);

		var md = PLAdmin.fsCache[data.fsid];
        for ( var aname in md.attributeMetaData)
        {
            var a = md.attributeMetaData[aname];
            //if ( a.type == '')
            PLAdmin.validate(errs, aname, data[aname], ((a.required&&a.required=="1")?PLAdmin.REQUIRED:PLAdmin.OPTIONAL)
                , 0, (a.size?parseInt(a.size):PLAdmin.ATTRIBUTE_TYPES[a.type].maxsize)
                , PLAdmin.ATTRIBUTE_TYPES[a.type].pattern, PLAdmin.ATTRIBUTE_TYPES[a.type].msg);
        }
        plDebug(errs);
        for ( var ae in errs)
        {
            var e = plById("error_" + ae + "_WidgetEle");
            plDebug("Error:" + ae + "=" + errs[ae]);
            if ( e ) e.innerHTML = errs[ae];
        }

        return errs;
    }

    , validate: function(errors, name, val, isrequired, minSize, maxSize, pattern, msg, options)
    {
        //debugger;
        plDebug("PLAdmin.validate name=" + name + " val=" + val + " isrequired="+ isrequired + " min=" + minSize + " max=" + maxSize + " pattern=" + pattern);
        var v = val +""; //convert to string
        var err = []; //erros for this field. The "errors" array is the errrors for all fields.
        if ( isrequired && isrequired === true && plIsEmpty(val)) err.push("required");
        else if ( !plIsEmpty(val) )
        {
            plDebug("not empty, check min size");
            if ( minSize && minSize > 0 && v.length < minSize ) err.push("must be at least " + minSize);
            plDebug("check max size");
            plDebug(v.length);
            if ( maxSize && maxSize > 0 && v.length > maxSize ) err.push("must be at most " + maxSize);
            plDebug("is there a pattern?");
            if ( !plIsEmpty(pattern))
            {
                plDebug("chek pattern");
                var p = new RegExp(pattern);
                if ( p.test(v) == false) err.push((msg?msg:"Invalid characters"));
                plDebug("pat done");
            }
        }
        plDebug("now check errors");
        if ( err.length > 0)
        {
            plDebug("has error");
            var t = err.slice(0);
            errors[name] = t;
            plDebug("PLAdmin.validate name=" + name + " ERROR=" + t);
        }
        plDebug("validate for " + name + " done");
    }
    , destroyWidgets: function()
    {
        for ( var w in PLAdmin.fe_widgets)
        {
            if ( PLAdmin.fe_widgets[w] && PLAdmin.fe_widgets[w].handle)
            {
                 if ( PLAdmin.fe_widgets[w].handle.destroy)  PLAdmin.fe_widgets[w].handle.destroy();
                 if ( PLAdmin.fe_widgets[w].type == 'location' ) PLAdmin.destroyLocationWidget();

                 PLAdmin.fe_widgets[w].handle = null;
            }
        }
    }
    //Gets the data from the widgets in the FeatureEditor
    , getInputData: function(clearErrorMsg)
    {
        var data = {};
        var v;
        var wid ;
        for ( var w in PLAdmin.fe_widgets)
        {
            v = "";
            wid = PLAdmin.fe_widgets[w];
            if ( wid && wid.handle)
            {
                switch ( wid.type)
                {
					case "texteditor":
                    case "simpleeditor":
                    case "richeditor": 
                        v = escape(plGetEditorWidgetValue(wid));
                        break;
                    case "location":
                        //get the location scheme into an attribute called _5flocation
                        data["_5flocationscheme"] = plById(wid.id + "_location_scheme").value;
                        //dont break, contine below to get the geojson
                    default:
                        v = plGetFormElementValue(wid.handle);
                }
                plDebug(wid.name + "=" + v  + " type=" + wid.type);
                data[wid.name] = v;
                if ( true == clearErrorMsg ) //clear error messages
                {
					var e1 = "error_" + wid.id;
                    var e = plById("error_" + wid.id );
                    if ( e) e.innerHTML = "";
                }
            }
        }
        return data;
    }


    , createWidget: function(key, type, parent, id, current_val, current_cssClass, attributes )
    {
		plDebug("createWidget, type = " + type + " , val = " + current_val + ", id=" + id );
		var val = current_val?current_val:"";
		var cssClass = current_cssClass?current_cssClass:"";
        var w = { "name": "", "type": "", "handle": null, "id": id };
        switch (type)
        {
            case "texteditor": // default text editor
            case "simpleeditor": // Simple Text Editor -- Fewer html controls on editor
            case "richeditor": // Rich Text Editor
                w["type"] = type;
                var e = plAddHtmlNode(parent, "textarea", val, {"id": id, "name" : key});
                attributes["id"] = id;
                w["handle"] = plCreateEditorWidget ( parent, attributes, type);
                break;
            case "textarea":
                w["type"] = "textarea";
                w["handle"] = plAddHtmlNode(parent, "textarea", val, {"id": id, "class": cssClass, "name" : key });
                break;
            case "select": //ListBox
                w["type"] = "select";
                w["handle"] = plAddHtmlNode(parent, "select", null, {"id": id, "class": cssClass, "name" : key});
                //add the options
                for (var o in attributes["options"])
                {
                    var opt = attributes["options"][o];
					var c = opt["code"] ? opt["code"] : o ;// handles regular or hash arrays
                    var p = {"value": c };
                    if ( val == c ) p["selected"]= "selected";
                    plAddHtmlNode(w["handle"], "option", opt["label"], p);
                }
                break;
            case "date":
                w["type"] = "date";
                w["handle"] = plAddHtmlNode(parent, "input", null, {"id": id, "type": "text", "class": cssClass, "value": val, "name" : key });
                //var b = plAddHtmlNode(parent, "button", "calendar", {"class": "plCalendarIcon", "onclick": "PLAdmin.onClickShowCalendar('" + key + "');"} )
                plAddHtmlNode(parent, "span", "(mm-dd-yyyy)", {"class": "plSmall"});
                //TODO add the calendar control
                break;
            case "hidden":
                w["type"] = "hidden";
                w["handle"] = plAddHtmlNode(parent, "input", null, {"id": id, "class": cssClass + " hidden", "value": val, "name" : key });
				// in IE* you can't change input elements type after adding it to the DOM, so ...
				//w["handle"].style.display = "none";
                break;
            case "location":
                w["type"] = "location";
                w["handle"] = PLAdmin.createLocationWidget(key, attributes["locationMode"], parent, id, val, attributes["location"]);
                break;
            default:
                //"textbox": //Textbox
                if ( type != 'textbox') plDebug("plCreateWidget unhandled type, " + type + " - rendering as textbox", "warn");
                w["type"] = "textbox";
                w["handle"] = plAddHtmlNode(parent, "input", null, {"id": id, "type": "text", "class": cssClass, "value": val, "name" : key });
        }
        plAddHtmlNode(parent, "div", null, { "id": ("error_" + id ),"class":"plError"});
        w["name"] = key;
        PLAdmin.fe_widgets[key] = w;
        plDebug("widget = " + w);
        return w;
    }

    , createFormField: function(key, container, mode, attrib, val)
    {
            /*var template = '<div id="${id}_FieldContainerEle">'
                            + '<div class="label">${label}</div>'
                            + '<div id="${id}_WidgetContainerEle>WIDGET HERE</div>'
                         + '</div>';
                         */
            var cont = plAddHtmlNode(container, "div", null, {"id": (key +"_FieldContainerEle"), "class" : "admin_fieldContainerEle"});
            var typ = PLAdmin.ATTRIBUTE_TYPES[attrib.type].widget;
			//plDebug("key="+key+", type="+typ);
            if ( typ != "hidden" ) plAddHtmlNode(cont, "div", attrib["label"], {"class": "label"});
            var widgetCont = plAddHtmlNode(cont, "div", null, {"id": (key+"_WidgetContainerEle")});
            //now create the widget
            var props = {};
            if ( attrib.type == 'h')
            {
              props["height"] = "400px";
              props["weight"] = "150px";
            }
            else if ( attrib.type == 'l')
            {
                props["options"] = attrib.options;
            }
            //var w = PLAdmin.createWidget(key, typ, widgetCont, (key + "_WidgetEle"), (attrib.value?attrib.value:val), null, props);
            var w = PLAdmin.createWidget(key, typ, widgetCont, (key + "_WidgetEle"), (attrib.value?attrib.value:val), null, attrib);
            return w;
    }

    , onClickShowCalendar : function(key)
    {
        plOpenCalendarWidget(PLAdmin.fe_widgets[key].handle);
    }

    , onClickEditLocation: function(id)
    {
        var div = plById(id + "_location_edit_container");
        if ( div) div.style.display = "block";
    }
    , onFocusLocationAddress : function(key, id, inputEle) //not used currently
    {
        //disable the buffer and buffer units until the geocode callback
        /*for ( var i=0; i< PLAdmin.LOCATION_ADDRESS_DEPENDENTS.length; i++)
        {
            var d = plById(id + PLAdmin.LOCATION_ADDRESS_DEPENDENTS[i]);
            if ( d) d.disabled = true;
        }*/
        //disable the done button
        PLAdmin._locationBefore = inputEle.value;

    }
    , onBlurLocationAddress: function(key, id, inputEle) //not used currently
    {
        var loc = inputEle.value;
        if ( !plIsEmpty(loc))
        {
            if ( loc != PLAdmin._locationBefore ) // location is dirty
            {
                pl_properties.geoCoder.call ( window, loc, PLAdmin.locationGeocodeCallback );
            }
            else
            {
                //enable any onfocus disable stuff
                /*for ( var i=0; i< PLAdmin.LOCATION_ADDRESS_DEPENDENTS.length; i++)
                {
                    var d = plById(id + PLAdmin.LOCATION_ADDRESS_DEPENDENTS[i]);
                    if ( d ) d.disabled = false;
                }*/
            }
        }
    }
    , onGeocodeLocationAddress: function(key, id, inputEle)
    {
        var loc = plById(id + "_location_address").value;
        if ( !plIsEmpty(loc))
        {
            PLAdmin._tempLocationRef = { "key": key, "id": id};
            pl_properties.geoCoder.call ( window, loc, PLAdmin.locationGeocodeCallback );
        }
    }
    , locationGeocodeCallback: function(geoCodedLocation )
	{
		plDebug("locationGeocodeCallback="+geoCodedLocation.toString());
        var ref = PLAdmin._tempLocationRef;
        var id = ref["id"];
        
		var radius = parseFloat(plCoalesce(plById(id + "_location_buffer").value, "0"));
        var units = plGetFormElementValue(plById(id + "_location_buffer_units"));
        if ( plIsEmpty(units)) units = "mile";
        plDebug("srid=" + pl_properties.geometrySrid + " units=" + units);
        //if ( units != 'mile')
        //{
            //convert radius to miles
            var f = 1;
            for ( var i=0; i< PLAdmin.UNITS.length; i++)
            {
                if ( PLAdmin.UNITS[i].code == units)
                {
                    f = PLAdmin.UNITS[i].factor[pl_properties.geometrySrid];
                    break;
                }
            }
            if ( f > 0) radius = radius * f;
            //debugger;
        //}
		//var conversionFactor = 69.1; // miles to degrees conversion, 1 degree = 69.1 miles
		//var radDegrees = radius / conversionFactor;

        var geom = new OpenLayers.Geometry.Point ( geoCodedLocation.lng(), geoCodedLocation.lat() );
        if ( radius > 0 ) //create ploygon, 20 approximates a circle based on OpenLayers doc
        {
            geom = OpenLayers.Geometry.Polygon.createRegularPolygon ( geom, radius, 20, 0 );
        }

        // convert the lat/lng to customer srid
        var proj = pl_admin_location_map.getProjectionObject();
        plDebug("map projection is" + proj.getCode());
        if ( proj.getCode() == "EPSG:900913" )
        {
            plDebug("transform from 4326 to 900913 srid");
            //Assumes the inistail
            var oldProj = new OpenLayers.Projection("EPSG:4326");
            geom = geom.transform( oldProj, proj);
        }
        else if ( proj.getCode() != "EPSG:4326")
        {
            plDebug("PLAdmin.locationGeocodeCallbak: unhandled srid:" + proj.getCode(), "warn");
        }
		PLAdmin.showGeometry(geom);
        plById(id + "_location_geojson").value = PLAdmin.toGeoJsonText(geom);
        var scheme = "a:" + plById(id + "_location_address").value + ":" + plById(id + "_location_buffer").value
                + ":" + plById(id + "_location_buffer_units").value;
        plById(id + "_location_scheme").value = scheme;
	}
    , toGeoJsonText : function(geometry)
    {
		return  PLAdmin.getGeoJsonFormatter().write(geometry) ;
    }
    , toGeometry: function(geojson)
    {
        var geom = PLAdmin.getGeoJsonFormatter().read(geojson);
        if ( geom && geom.length > 0 ) return geom[0].geometry;
    }
	, showGeometry: function(geometry)
	{
		plDebug("in PLAdmin.showGeometry");// geometry = " + geometry);
        if ( !geometry )
        {
            plDebug("PLAdmin.showGeometry: geometry is null", "warn");
            return;
        }
        var m = pl_admin_location_map;
		var fLayer = new OpenLayers.Layer.Vector("feature");
		m.addLayer(fLayer);
        //plDebug("added layer");
		var feat = new OpenLayers.Feature.Vector( geometry );
        //plDebug("created feature");
		fLayer.addFeatures([feat]);
        //plDebug("added feat");
        m.zoomToExtent ( geometry.getBounds() ); 
        plDebug("geometry plot and zoom done");
	}
    , createLocationWidget: function(key, mode, parent, id, geojson, location)
    {
        var loc = PLAdmin.parseLocationScheme(location);
        var hideEdit = false;
        plDebug("in createLocationWidget mode=" + mode);
        if ( mode == 'view')
        {
            var msg = "";
            if (  'm' == loc.method )
                msg = "Drawn on map";
            else if ( 'f' == loc.method )
                msg = "Preset region";
            else
                msg = loc.address + (loc.buffer!=''?(' + ' + loc.buffer + loc.bufferUnits):'');

            plAddHtmlNode(parent, "div", msg , {"class": "plnormal"} );
            plAddHtmlNode(parent, "a", "edit", {"href": "javascript:PLAdmin.onClickEditLocation('" + id + "')"});
            hideEdit = true;
        }
        var editcont = plAddHtmlNode(parent, "div", null, {"id": (id + "_location_edit_container"), "style": ("display:" + (hideEdit?"none;":"block;") )} );
        if (  'a' == loc.method) //address based location
        {
            plAddHtmlNode(editcont, "input", null, {"id": (id + "_location_address"), "type":"text"
                    , "size": "30", "value": loc.address
                    //, "onblur": ("PLAdmin.onBlurLocationAddress('" + key + "','" + id + "',this)")
                    //, "onfocus": ("PLAdmin.onFocusLocationAddress('" + key + "','" + id + "',this)")
                    } );
            plAddHtmlNode(editcont, "input", null, {"id": (id + "_location_buffer"), "value": loc.buffer, "type":"text", "size": "8", "style": "display:none"} ); //TODO, hidden for now
            var sel = plAddHtmlNode(editcont, "select", null, {"id": (id + "_location_buffer_units"), "style": "display:none"} ); //TODO, hidden for now
                for ( var i=0; i< PLAdmin.UNITS.length; i++)
                {
                    var opt = { "value": PLAdmin.UNITS[i].code };
                    if ( PLAdmin.UNITS[i].code == loc.bufferUnits ) opt["selected"] = "selected";
                    plAddHtmlNode(sel, "option", PLAdmin.UNITS[i].label, opt);
                }
           plAddHtmlNode(editcont, "button", "geocode", {"onclick": ("PLAdmin.onGeocodeLocationAddress('" + key + "','" + id + "',this)")} );
        }
        else
        {
            plDebug("PLAdmin.createLocationWidget: mthod" + loc.method + " is not yet implemented", "warn");
        }        
        var wid = plAddHtmlNode(parent, "input", null, {"id": (id + "_location_geojson"), "class":"hidden",  "value": geojson } );
		// IE8 can't do type=hidden afterwards so ...
		//wid.style.display = "hidden";
        var dummy = plAddHtmlNode(parent, "input", null, {"id": (id + "_location_scheme"), "class" : "hidden", "value": location} );
		//dummy.style.display = "hidden";
        plAddHtmlNode(parent, "div", null, {"id": (id + "_location_map"), "class": "plFeatureEditorLocationMap" });
        PLAdmin.initLocationMap((id + "_location_map"));
        if ( !plIsEmpty(geojson) )
        {
            PLAdmin.showGeometry(PLAdmin.toGeometry(geojson));
        }
        return wid;

    }
    , initLocationMap: function(contId)
    {
        var m = pl_admin_location_map;//PLAdmin.featureMap
        if ( m )
        {   // reuse it
            var num = m.getNumLayers();
            while (num > 0)
			{
				m.removeLayer( m.layers[0] );
                num = m.getNumLayers();
			}
		}
		else
		{   // create it
			var defaultProps = pl_properties.defaultRunProperties;
			var props = {
				map: {  enabled : true
					, container: contId
					, layerName: "Feature"
					, zoom: defaultProps.map.zoom
					, defaultCenter : { lng: pl_properties.center.lng, lat: pl_properties.center.lat }
					, mapObjectName : "pl_admin_location_map" //"PLAdmin.featureMap"
					, setBaseLayer : defaultProps.map.setBaseLayer
					, WMSLayerUrl : defaultProps.map.WMSLayerUrl

				}
			};
			plLoadBaseMap ( props );
		}        
    }
    , destroyLocationWidget: function()
    {
        var m = pl_admin_location_map;//PLAdmin.featureMap
        if ( m )
        {   // reuse it
            var num = m.getNumLayers();
            while (num > 0)
			{
				m.removeLayer( m.layers[(num-1)] );
                num = m.getNumLayers();
			}
            m.destroy();
           pl_admin_location_map = null;
        }

    }
    , parseLocationScheme: function(location)
    {
            var loc = { "method": "", "address": "", "buffer": "", "bufferUnits": "", "fsid": "", "fid": "" };
            var parsedloc = (location?location.split(":"):["a"]);
            if ( parsedloc && parsedloc.length > 0 )
            {
                if ( parsedloc[0] ) loc.method = parsedloc[0];
                if ( loc.method == 'a') //address
                {
                    if ( parsedloc.length > 1 && parsedloc[1]) loc.address = parsedloc[1];
                    if ( parsedloc.length > 2 && parsedloc[2]) loc.buffer = parsedloc[2];
                    if ( parsedloc.length > 3 && parsedloc[3]) loc.bufferUnits = parsedloc[3];
                }
                else if ( loc.method == 'f')
                {
                    if ( parsedloc.length > 1 && parsedloc[1]) loc.fsid = parsedloc[1];
                    if ( parsedloc.length > 2 && parsedloc[2]) loc.fid = parsedloc[2];
                }
            }
            return loc;
    }
};
/***  end of PLAdmin stuff **/        plLoadStyles("/res/pl/admin.css");
                /*** **********************  Facebook login stuff  ******************************************/
plDebug("loading Facebook stuff");
var pl_User = {  "id": "0", "email": "", "providerCode": ""
                , "externalId" : "", "externalSessionId" : ""
                , "createdAt"  : "", "status" : "", "statusDate"  : ""
                , "lastLogin"   : ""
                , "profile"     :
                {
                    name : "Guest"
                  , email : "UNAVAILABLE"
                  , externalId : "pl:anonymous"
                  , locations : { "items" : []}
                  , friends :  { "items" : []}
                  , groups :  { "items" : []}
                  , networks :  { "items" : []}
                  , url : { "pic": "", "web": "" }
                }
                , state : [] // processing state clide side only
};

var PLAuth = {
        callbackName : ""
        , hasFBGetUserBeenCalled : false // in some cases, getFBUser is being called twice. Not sure why. As a hack, lets return if its already been invoked
        , startAuthenticationCheck: function(cbName)
        {
            PLAuth.callbackName = cbName;
            FB_RequireFeatures(["XFBML"], function()
            {
                plDebug("in req features");
                FB.init(pl_properties["identityProviderApiKey"], "/xd_receiver.htm");
                FB.Facebook.apiClient.users_getLoggedInUser(function(result, ex)
                {
                    plDebug("is user logged in?");
                    plDebug(result);
                    if ( result && result != '')
                    {
                         PLAuth.getFBUser(result);
                    }
                    else
                    {
                        plDebug("user is not logged in,");
                        PLAuth.afterUserIsLoaded();
                    }
                });
                FB.Facebook.get_sessionState().waitUntilReady(function()
                {
                    plDebug("Session is ready");
                    var fbSession = FB.Facebook.apiClient.get_session();
                    PLAuth.getFBUser(fbSession.uid);
                });
            });
        }

        //Gets the user profile info from Facebook
        ,getFBUser: function(uid)
        {
                // in some cases, getFBUser is being called twice. Not sure why. As a hack, lets return if its already been invoked
                if ( PLAuth.hasFBGetUserBeenCalled == true ) return;
                PLAuth.hasFBGetUserBeenCalled = true;

                pl_User.state = [0,0];
				//debugger;
                pl_User.externalId = uid;
                FB.Facebook.apiClient.fql_query(
                    "select name, pic_square, pic, profile_url, current_location, affiliations from user where uid = " + uid
                    , function(result, ex)
                    {
                        //plDebug("User query=");
                        //plDebug(result);
                        if ( result && result.length> 0 )
                        {
                            //plDebug("saving pl stuff");
                            //plDebug(result[0]["first_name"]);
                             pl_User.profile.name = result[0]["name"];
                             pl_User.profile.url = { smallpic : result[0]["pic_square"], pic :result[0]["pic"], web : result[0]["profile_url"] };
                             pl_User.profile.locations.items.push(result[0]["current_location"]);
                             for ( var i=0; i< result[0].affiliations.length; i++)
                             {
                                //pl_User.profile.networks.items.push({"name": result[0].affiliations[i]["name"], "gid": result[0].affiliations[i]["gid"]});
                                pl_User.profile.networks.items.push(result[0].affiliations[i]["gid"]);
                             }
                        }
                        pl_User.state[0] = 1;
                        if ( pl_User.state[0] == 1 && pl_User.state[1] == 1)
                        {
                            PLAuth.fbUserIsLoaded();
                        }
                    }
                );
                // Note: we are only looking for the Group we are interested in. TODO change this to a IN clause, so that we can multiple groups
                FB.Facebook.apiClient.fql_query(
                    "select positions from group_member where uid= " + uid + " and gid = " + pl_properties["adminAuthorizationGroup"]
                    , function(result, ex)
                    {
                        plDebug("group query=");
                        plDebug(result);
                        if ( result )
                        {
                             //pl_User.canedit = true;
                             pl_User.profile.groups.items.push(pl_properties["adminAuthorizationGroup"]);
                        }
                        pl_User.state[1] = 1;
                        if ( pl_User.state[0] == 1 && pl_User.state[1] == 1)
                        {
                            PLAuth.fbUserIsLoaded();
                        }
                    }
                );

        }

        //after both FB queries are completed and processed, get the PL uid
        , fbUserIsLoaded: function()
        {
            //debugger;
            plDebug("User is loaded" + pl_User.state[0] + ":" + pl_User.state[1]);
            if ( pl_User.externalId != '')
            {
                //Get the PlaceLogic ID
                PLAdmin.getPlacelogicUserProfile("PLAuth.getPlaceLogicUserProfileCallback", pl_User);
            }
            else
            {
                PLAuth.afterUserIsLoaded();
            }

        }

        , getPlaceLogicUserProfileCallback: function(resp)
        {
            if ( resp.response.status.code == 'SUCCESS')
            {
               pl_User = resp.response.content.userContext;
               PLAuth.afterUserIsLoaded();
            }
            else
            {
                throw new Error("PlaceLogic Profile lookup failed:" + resp.response.status.message);
            }
        }
        , afterUserIsLoaded : function()
        {
			pl_User.facebookIsLoaded = true;
            if ( PLAuth.callbackName && PLAuth.callbackName != '' && window[PLAuth.callbackName] )
            {
                window[PLAuth.callbackName].call(window);
            }
        }
/*
        , registerUser : function(email)
        {
            var norm = email.toLowerCase().trim();
            var crc = crc32(norm);
            var md = MD5(norm);
            emailhash = "{" + crc + "_" + md + "}";
            Connect.registerUsers(apiKey, sig, 1.0, emailhash);
        }
*/
/**
 * Register new accounts with Facebook to facilitate friend linking.
 * Note: this is an optional step, and only makes sense if you have
 * a site with an existing userbase that you want to tie into
 * Facebook Connect.
 *
 * See http://wiki.developers.facebook.com/index.php/Friend_Linking
 * for more details.
 *
 * @param $accounts  array of accounts, each with keys 'email_hash' and 'account_id'
 * @return whether the emails were registered. true unless there's an error

function facebook_registerUsers($accounts) {
  $facebook = facebook_client();
  $session_key = $facebook->api_client->session_key;
  $facebook->api_client->session_key = null;

  $result = false;
  try {
    $ret = $facebook->api_client->call_method(
             'facebook.connect.registerUsers',
             array('accounts' => json_encode($accounts)));

    // On success, return the set of email hashes registered
    // An email hash will be registered even if the email does not match a Facebook account

    $result = (count($ret) == count($accounts));
  } catch (Exception $e) {
    error_log("Exception thrown while calling facebook.connect.registerUsers: ".$e->getMessage());
  }

  $facebook->api_client->session_key = $session_key;
  return $result;
}


 * Lets Facebook know that these emails are no longer members of your site.
 *
 * @param email_hashes   an array of strings from registerUsers

function facebook_unregisterUsers($email_hashes) {
  $facebook = facebook_client();
  $session_key = $facebook->api_client->session_key;
  $facebook->api_client->session_key = null;

  // Unregister the account from fb
  $result = false;
  try {
    $ret = $facebook->api_client->call_method(
             'facebook.connect.unregisterUsers',
             array('email_hashes' => json_encode($email_hashes)));
    $result = (count($email_hashes) == count($ret));
  } catch (Exception $e) {
    error_log("Exception thrown while calling facebook.connect.unregisterUsers: ".$e->getMessage());
  }

  $facebook->api_client->session_key = $session_key;
  return $result;
        
}
*/


};
/***  enf of FB stuff **/
                                /* 
 * Google Maps
 */
	function plLazyLoadGoogleMapsAPI ( key, callbackFunctionName ) // callbackFunctionName needs to be a string
	{
		plLoadScript("http://maps.google.com/maps?file=api&v=2&key=" + key + "&async=2&callback=" + callbackFunctionName);
	}
	/*
	function plLazyLoadGoogleMapsAPI ()
	{
		console.debug("google maps api loaded");
	}
	*/
    function plSetGoogleBaseLayer(props)
    {
        //debugger;
        props.map.mapObject.addLayers([new OpenLayers.Layer.Google(
                "Google Physical",
                {
					type: G_PHYSICAL_MAP
					, 'isBaseLayer': true
					, "sphericalMercator": true
					, 'maxExtent': new OpenLayers.Bounds(-20037508.34,-20037508.34,20037508.34,20037508.34)
					, 'maxResolution': 156543.0339
				}
            )
            
			, new OpenLayers.Layer.Google(
                "Google Streets", // the default
                {numZoomLevels: 20
					, 'isBaseLayer': true
					, "sphericalMercator": true
					, 'maxExtent': new OpenLayers.Bounds(-20037508.34,-20037508.34,20037508.34,20037508.34)
					, 'maxResolution': 156543.0339
				
				}
            )
            , new OpenLayers.Layer.Google(
                "Google Hybrid",
                {type: G_HYBRID_MAP, numZoomLevels: 20
					, 'isBaseLayer': true
					, "sphericalMercator": true
					, 'maxExtent': new OpenLayers.Bounds(-20037508.34,-20037508.34,20037508.34,20037508.34)
					, 'maxResolution': 156543.0339
				}
            )
            , new OpenLayers.Layer.Google(
                "Google Satellite",
                {type: G_SATELLITE_MAP, numZoomLevels: 20
					, 'isBaseLayer': true
					, "sphericalMercator": true
					, 'maxExtent': new OpenLayers.Bounds(-20037508.34,-20037508.34,20037508.34,20037508.34)
					, 'maxResolution': 156543.0339
			}
            )
			

        ]);
		
    }

   function plGoogleGeocoder(address, callback )
   {
       var geocoder = new GClientGeocoder(); geocoder.getLatLng(address, callback) ;
   }
                                /* 
 * WMS
 */
    function plSetWMSBaseLayer(props)
    {
        var layer = new OpenLayers.Layer.WMS( "OpenLayers WMS",
            props.map.WMSLayerUrl,
            {layers: 'basic'}, 
			{
				"attribution": props.map.attribution
				, projection: new OpenLayers.Projection("EPSG:900913") 
				} );
        props.map.mapObject.addLayer(layer);
    }

    function plWMSGeocoder(address, callback )
    {
        alert ('plWMSGeocoder TODO');
    }
                plLoadScript("/placelogic.php?/placelogic/services/subscribe/json/run.action?version=1.0&callid=" + plGetNewCallId() + "&apikey=ayu723rgsfvdh29f0sbx3l7s4601gv5jskr7lvvb6szkl9j530", {onsuccess: function(){ plPublish("load", { message: "loaded_plsubcribeservice"} );}});

