/* DO NOT DE-TAB! spaces will increase the weight of this file by ~20%  */
document.write("<sc"+"ript  type='text/javascript' src='/js/dom.js'></script>");
document.write("<sc"+"ript  type='text/javascript' src='/js/ajax.js'></script>");

function Mappable(id,longitude,latitude) {
    this.id = id;
    this.index = null;
    this.point = new GPoint(longitude,latitude);
    /**populates the marker */
    this.infoHTML = "<img src=\"/images/search/loading.gif\" />";
    /**search result */
    this.searchResult = null;

    this.isComplete = function() {
        return !(!this.searchResult);
    }

    this.getMarker = function(icon) {
        var marker = this.marker;
        var mappableController = new MappableController();
        var markerIcon = icon||mappableController.getOutIcon(0,this);
        var map = mappableController.map;
        if (this.point == null || this.point.x == -9090909 || this.point.y == -9090909) {
            marker = null;
        } else {
            marker = new GMarker(this.point, markerIcon);
            marker.mappableID = this.id;
            marker.openInfoWindow = function() {
                var mappable = new MappableManager().getLiteMappable(marker.mappableID);
                marker.openInfoWindowHtml(mappable.infoHTML);
                map.openInfoWindowMarker = marker;
                var searchResultElement = document.getElementById("searchResult" + mappable.id);
                //highlight
                if (searchResultElement) {
                    mappableController.clearHilight();
                    searchResultElement.className = "highlight";
                    //scroll (this is a bit of a hack since we know at [initial] design time that it's the parent div that scrolls)
                    var scroller = DomUtil.getParentNodeOfType(mappableController.searchResultsParentElement,"div");
                    //hack for the "IE horizontal scroll hack"
                    scroller = DomUtil.getParentNodeOfType(scroller,"div");
                    var isInViewableArea = (searchResultElement.offsetTop > scroller.scrollTop && searchResultElement.offsetTop < (scroller.scrollTop + parseInt(scroller.style.height)));
                    if (!isInViewableArea) {
                        scroller.scrollTop = searchResultElement.offsetTop;
                    }
                }
                if (!mappable.isComplete()) {
                    //if not complete then get the mappable, and have the callback open the info window
                    var callback = function(mappable) {
                        map.closeInfoWindow();
                        marker.openInfoWindowHtml(mappable.infoHTML);
                    }
                    var mappableManager = new MappableManager();
                    var mappable = mappableManager.getMappableAndCallBack(callback,mappable.id);
                }
            }
            GEvent.addListener(marker, "click", function() {
                marker.openInfoWindow();
            });
        }
        return marker;
    }
}
var loc = window.location;
var locParts = loc.hostname.split(".");
var prefix = (locParts.length > 3)?locParts[0] + ".":"";
var port = (loc.port)?":8080":"";
var MARKER_HOME = "http://"+prefix+"www.commercialiq.com"+port+"/images/map/";
var base = new GIcon();
base.shadow = MARKER_HOME + "shadow50.png";
base.iconSize = new GSize(28, 32);
base.shadowSize = new GSize(37, 34);
base.iconAnchor = new GPoint(9, 34);
base.infoWindowAnchor = new GPoint(9, 2);
base.infoShadowAnchor = new GPoint(18, 25);
Mappable.inListIcon = new GIcon(base);
Mappable.inListIcon.image = MARKER_HOME + "marker_red.png";
Mappable.inListIcon.iconSize = new GSize(28, 32);
Mappable.inListIcon.shadow = MARKER_HOME + "marker_shadow.png";
Mappable.inListIcon.shadowSize = new GSize(48, 31); 
Mappable.outListIcon = new GIcon(base);
Mappable.outListIcon.image = MARKER_HOME + "marker_red.png";

function MappableManager(mappableType) {
    if (! MappableManager.instance) {
        MappableManager.instance = this;
    } else {
        return MappableManager.instance;
    }
    this.mappableType = mappableType;
    this.searchType = mappableType + "Search";
    this.search = eval("new " + this.searchType + "()");

    /** hash of Mappables indexed by ID */
    this.mappablesByID = new Array();

    /** array of ints */
    this.mappableIDsInOrder = new Array();

    this.addMappable = function(mappable,index) {
        if (mappable["id"] && mappable["point"]) {
            //in case something other than a Mappable gets in here since javascript doesn't allow us to determine the type
            var name = new String(mappable.id);
            var currentMappableInCache = this.getLiteMappable(mappable.id);
            if (!currentMappableInCache || mappable.isComplete()) {
                this.mappablesByID[name] = mappable;
            }
            if (!currentMappableInCache) {
                this.mappableIDsInOrder.push(name);
            }
            if (index == 0 || index) {
                mappable.index = index;
            }
        }
    }

    this.removeMappable = function(mappable) {
        var name = new String(mappable.id);
        this.mappablesByID[name] = null;
        mappable.index = null;
    }

    /** returns true if we have this mappable in the cache */
    this.hasMappables = function() {
        var ids = unpackArguments(arguments);
        var hasMappables = true;
        for (var i=0;i<ids.length;i++) {
            var mappable = this.getLiteMappable(ids[i]);
            hasMappables = mappable && mappable.isComplete();
            if (!hasMappables) {
                break;
            }
        }
        return hasMappables;
    }

    /** gets a mappable that has an info window or searchResult */
    this.getMappableAndCallBack = function(callback,id) {
        //the callback needs to unpack the returned mappables
        var modifiedCallback = function(mappables) {
            var mappable = null;
            if (mappables.length) {
                mappable = mappables[0];
            }
            callback(mappable);
        }
        this.getMappablesAndCallBack(modifiedCallback,id);
    }

    /** takes an array of ids or variable length arguments of ids */
    this.getMappablesAndCallBack = function(callback) {
        var ids = unpackArguments(arguments,1);
        //determine which ones we don't have
        var idsWeDontHave = new Array();
        for (var i=0;i<ids.length;i++) {
            if (!this.hasMappables(ids[i])) {
                idsWeDontHave.push(ids[i]);
            }
        }

        //modify the callback so that it will first load the retrieved mappables into the MappableManager
        var modifiedCallback = function(retrievedMappables) {
            var mappableManager = new MappableManager();
            for (var i=0;i<retrievedMappables.length;i++) {
                mappableManager.addMappable(retrievedMappables[i]);
            }

            //create an array of all of the Mappables asked for and pass it to the callback
            var requestedMappables = new Array();
            for (var i=0;i<ids.length;i++) {
                var mappable = mappableManager.getLiteMappable(ids[i]);
                if (mappable) {
                    requestedMappables.push(mappable);
                }
            }
            callback(requestedMappables);
        }

        //run the search.  Once it's done, the callback will be called
        this.search.searchByIDs(idsWeDontHave,modifiedCallback);
    }

    /** gets a mappable from the cache that may or may not have an info window or searchResult */
    this.getLiteMappable = function(id) {
        var name = new String(id);
        var mappable = this.mappablesByID[name];
        return mappable;
    }

    this.clear = function() {
        this.mappablesByID = new Array();
        this.mappableIDsInOrder = new Array();

    }

}
MappableManager.instance = null;

function MappableController(map,searchResultsParentElement, throbberElement, mappableType) {
    if (! MappableController.instance) {
        MappableController.instance = this;
    } else {
        return MappableController.instance;
    }

    this.map = map;
    this.mappableManager = new MappableManager(mappableType);
    var mappableIDFieldManager = new MappableIDFieldManager();
    if (mappableIDFieldManager.form) {
        //hack since the MappableIDFieldManager is a singleton, but it only works if it was already initialized with a form
        this.mappableIDFieldManager = mappableIDFieldManager;
    }
    this.currentPage = 0;
    this.searchResultsParentElement = searchResultsParentElement
    this.throbberElement = throbberElement;

    /** array of ints */
    this.visibleMappableIDsInOrder = new Array();

    this.addMappable = function(mappable,index) {
        this.mappableManager.addMappable(mappable,index);
        this.map.addOverlayByName(mappable.id,mappable.getMarker());
        this.visibleMappableIDsInOrder.push(mappable.id)
    }

    this.removeVisibleMappable = function(mappable,doNotRerenderSearchResult) {
        this.clearVisibleMappable(mappable,doNotRerenderSearchResult);
    }

    this.clearVisibleMappable = function(mappable,doNotRerenderSearchResult) {
        this.map.removeOverlayByName(mappable.id);

        //remove the mappable ids from this.visibleMappableIDsInOrder
        if (!mappable.index && mappable.index != 0) {
            //if there is no index, then reindex the mappables.
            this.indexMappables();
        }
        this.visibleMappableIDsInOrder.splice(mappable.index,1);

        if (!doNotRerenderSearchResult) {
            this.showSearchResultsForPage(this.currentPage);
        }

        //remove hidden form field representing this id if it exists
        if (this.mappableIDFieldManager) {
            this.mappableIDFieldManager.removeHiddenFieldForMappableID(mappable.id);
        }
    }

    this.clear = function() {
        this.mappableManager.clear();
        for(var i=0;i<this.mappableIDsInOrder.length;i++) {
            this.removeVisibleMappable(this.mappableManager.getLiteMappable(this.mappableIDsInOrder[i]),true);
        }
        this.currentPage = 0;
        this.visibleMappableIDsInOrder = new Array();
    }

    this.clearAllVisibleMappables = function() {
        this.indexMappables();
        for(var i=this.visibleMappableIDsInOrder.length-1;i>=0;i--) {
            this.clearVisibleMappable(this.mappableManager.getLiteMappable(this.visibleMappableIDsInOrder[i]),true);
        }
        this.showSearchResultsForPage(1);
    }

    this.getMappableIDsForPage = function(page) {
        page = page||this.currentPage;
        var ids = null;
        if (page < 0) {
            ids = this.visibleMappableIDsInOrder;
        } else {
            var start = (page - 1) * MappableController.LISTINGS_PER_PAGE;
            var end = start + MappableController.LISTINGS_PER_PAGE;
            ids = this.visibleMappableIDsInOrder.slice(start, end);
        }
        return ids;
    }

    /** goes to the MappableManager to get Mappables.  The MappableManager may in turn run a search */
    this.getMappablesForPageAndCallBack = function(page,callback) {
        return this.mappableManager.getMappablesAndCallBack(callback,this.getMappableIDsForPage(page));
    }

    this.indexMappables = function() {
        var mappableIDs = this.visibleMappableIDsInOrder;
        for (var i=0;i<mappableIDs.length;i++) {
            var mappable = this.mappableManager.getLiteMappable(mappableIDs[i]);
            mappable.index = i;
        }
    }

    this.sortMappables = function(sortField) {
        //need to replace the visibleMappableIDs with the results of a search
        var search = this.mappableManager.search;
        if (search["searchWithSort"]) {
            //not all searches will have sorts
            this.throb();
            var callback = function(sortedMappables) {
                var mappableController = new MappableController();
                mappableController.clearAllVisibleMappables();
                this.visibleMappableIDsInOrder = new Array();
                for (var i=0;i<sortedMappables.length;i++) {
                    var mappable = sortedMappables[i];
                    mappableController.addMappable(sortedMappables[i])
                }
                mappableController.showSearchResultsForPage(1);
                mappableController.unthrob();
            }
            var sortedMappables = search.searchWithSort(this.visibleMappableIDsInOrder ,sortField,callback);
        }
    }

    this.clearHilight = function() {
        //this is cheap.  rather, we should keep track of which row is hilighted.
        var tbodies = DomUtil.getChildNodesOfType(this.searchResultsParentElement,"tbody");
        for (var i=0;i<tbodies.length;i++) {
            var tbody = tbodies[i];
            if (tbody.className == "highlight") {
                tbody.className = "odd";
                break;
            }
        }
    }

    //override these methods to get custom icons
    this.getInIcon = function(index, mappable) {
        var icon = new GIcon(Mappable.inListIcon);
        if (index < 100) {
            icon.image = icon.image.replace(".png","-"+eval(index+1)+".png");
        }
        return icon;
    }
    this.getOutIcon = function(index, mappable) {
        return Mappable.outListIcon;
    }

    this.synchronizeMapDisplay = function(oldPage,newPage) {
        //reset markers for old page
        if (oldPage) {
            var ids = this.getMappableIDsForPage(oldPage);
            for (var i=0;i<ids.length;i++) {
                var mappable = this.mappableManager.getLiteMappable(ids[i]);
                this.map.replaceOverlayByName(ids[i],mappable.getMarker(this.getOutIcon(i,mappable)));
            }
        }

        //set markers for new page
        if (newPage) {
            var ids = this.getMappableIDsForPage(newPage);
            for (var i=0;i<ids.length;i++) {
                var mappable = this.mappableManager.getLiteMappable(ids[i]);
                this.map.replaceOverlayByName(ids[i],mappable.getMarker(this.getInIcon(i,mappable)));
            }
        }
    }

    this.showInfoWindowForMappableID = function(id) {
        var mappable = this.mappableManager.getLiteMappable(id);
        if (mappable && mappable.isComplete()) {
            this.map.toggleInfoWindowByName(mappable.id);
        } else {
            var callback = function(mappable) {
                new MappableController().map.toggleInfoWindowByName(mappable.id);
            }
            this.mappableManager.getMappableAndCallBack(callback,id);
        }
    }

    this.showSearchResultsForPage = function(page,oldPage,externalCallback) {
        if (this.searchResultsParentElement) {
            oldPage = oldPage||this.currentPage;
            page = page||this.currentPage;
            this.currentPage = page;

            //create a callback that the MappableManager will call once it knows that it has the required Mappables available
            var callback = function(mappables) {
                var mappableController = new MappableController();
                //need to remove the current table that search results live in and replace it with a new one.  We'll do this by building up a string and inserting it with innerHTML since our rows are tbody elements which IE will not let us insert via innerHTML
                var tableParent = DomUtil.getNonTextParentNode(mappableController.searchResultsParentElement);
                var newTableString = '<table border="0" cellspacing="0" cellpadding="0" id="' + mappableController.searchResultsParentElement.id + '">\n';

                //build up search results display
                for (var i=0;i<mappables.length;i++) {
                    var mappable = mappables[i];
                    newTableString += mappable.searchResult + "\n" ;
                }
                newTableString + "</table>"

                //remove old table
                tableParent.removeChild(mappableController.searchResultsParentElement);
                //add the new  table
                tableParent.innerHTML = newTableString;
                mappableController.searchResultsParentElement = DomUtil.getChildNodesOfType(tableParent,"table")[0]

                //now update the table with the correct properties
                for (var i=0;i<mappables.length;i++) {
                    var mappable = mappables[i];
                    var tbody = document.getElementById("searchResult" + mappable.id);
                    var className = "odd";
                    if (mappableController.map.openInfoWindowMarker && mappableController.map.openInfoWindowMarker.mappableID == mappable.id) {
                        className = "highlight";
                    }
                    $('tbody:odd').addClass(className);
                    $('tbody:even').addClass("even");
                    //tbody.className = className;
                    //set the icon correctly
                    document.getElementById("searchResultIcon" + mappable.id).src = mappableController.getInIcon(i,mappable).image;
                }

                mappableController.unthrob();

                //if there's only one guy, then show him
                if (mappableController.visibleMappableIDsInOrder.length == 1) {
                    this.map.toggleInfoWindowByName(mappableController.visibleMappableIDsInOrder[0])
                }
                mappableController.synchronizeMapDisplay(oldPage,page);

                //take care of anything that should be done after the pages switch
                if (mappableController.mappableIDFieldManager) {
                    mappableController.mappableIDFieldManager.afterSearchResultChange();
                }

                new StateManager().serializeIDs();
                if (externalCallback) {
                    externalCallback();
                }
            }
            this.throb();
            this.getMappablesForPageAndCallBack(page,callback)
        }
    }

    this.throb = function(container) {
        //note that we need to go up two parents.  this is because the mappables were wrapped in an extra div.  if the throbber is out of place, look here.
        container = container || this.searchResultsParentElement;
        var pos = DomUtil.getRealPosition(container);
        throbberElement.style.top = pos.y + 40 + "px";
        var horizontalPad = Math.round((container.offsetWidth - this.throbberElement.offsetWidth)/2);
        throbberElement.style.left = pos.x + horizontalPad + "px";
        throbberElement.style.visibility = "visible";
    }

    this.unthrob = function() {
        this.throbberElement.style.visibility = "hidden";
    }

    this.refineByPolygon = function(polygon) {
        polygon.close();
        if (this.mappableIDFieldManager) {
            this.mappableIDFieldManager.beforeSearchResultChange();
        }
        var ids = this.visibleMappableIDsInOrder;
        for (var i=ids.length-1; i>=0;i--) {
            var mappable = this.mappableManager.getLiteMappable(ids[i]);
            if (mappable.point && !polygon.containsPoint(mappable.point)) {
                this.removeVisibleMappable(mappable,true);
            }
        }
        this.refreshAfterRefine();
        polygon.zoomToFit();
    }

    this.refineByChecked = function() {
        if (this.mappableIDFieldManager) {
            this.mappableIDFieldManager.beforeSearchResultChange();
        }
        var ids = this.visibleMappableIDsInOrder;
        for (var i=ids.length-1;i>=0;i--) {
            if (this.mappableIDFieldManager && ! this.mappableIDFieldManager.isSelected(ids[i])) {
                var mappable = this.mappableManager.getLiteMappable(ids[i]);
                this.removeVisibleMappable(mappable,true);
            }
        }
        this.refreshAfterRefine();
    }

    this.refineAll = function(func) {
        this.indexMappables();
        var ids = mapFunction(function(id){return id},this.mappableManager.mappableIDsInOrder);
        var newMappablesArray = new Array();
        var index = 0;
        this.clearAllVisibleMappables();
        for (var i=0;i<ids.length;i++) {
            var mappable = this.mappableManager.getLiteMappable(ids[i]);
            var shouldBeVisible = func(mappable);
            if (shouldBeVisible) {
                this.addMappable(mappable,index);
                index++;
            }
        }
        this.refreshAfterRefine();
    }

    this.refreshAfterRefine = function() {
        //update the index
        this.indexMappables();

        this.currentPage = 0;
        this.showSearchResultsForPage(1,0);
        new PaginationManager().refresh();

    }

    this.startOver = function() {
        this.clearAllVisibleMappables();

        var ids = this.mappableManager.mappableIDsInOrder;
        for (var i=0;i<ids.length;i++) {
            var mappable = this.mappableManager.getLiteMappable(ids[i]);
            this.addMappable(mappable,i);
            if (this.mappableIDFieldManager) {
                this.mappableIDFieldManager.removeHiddenFieldForMappableID(ids[i]);
            }
        }
        this.refreshAfterRefine();
        this.zoomToFitMappables();
    }

    this.zoomToFitMappables = function(minZoom,maxZoom) {
        var north = 0
        var south = 0
        var east = 0
        var west = 0;
        var ids = this.visibleMappableIDsInOrder;
        for (var i=0;i<ids.length;i++) {
            var mappable = this.mappableManager.getLiteMappable(ids[i]);
            if (mappable && mappable["point"]) {
                var pt = mappable.point;
                if (pt.y != -9090909 && (!north || pt.y > north)) {
                    north = pt.y;
                }
                if (pt.y != -9090909 && (!south || pt.y < south)) {
                    south = pt.y;
                }
                if (pt.x != -9090909 && (!east || pt.x > east)) {
                    east = pt.x;
                }
                if (pt.x != -9090909 && (!west || pt.x < west)) {
                    west = pt.x;
                }
            }
        }
        if (this.visibleMappableIDsInOrder.length) {
            this.map.zoomToFit(north,south,east,west,minZoom,maxZoom);
        }
    }
}
MappableController.LISTINGS_PER_PAGE = 25;
MappableController.instance = null;

/** maintains the state of which mappables have been checked while paging through search results  */
function MappableIDFieldManager(form) {
    if (MappableIDFieldManager.instance) {
        return MappableIDFieldManager.instance;
    }
    MappableIDFieldManager.instance = this;
    this.form = form;

    this.beforeSearchResultChange = function() {
        //find all checked mappableID checkboxes
        var mappableIDElements = this.form["listingID"] || this.form["mappableID"];
        if (mappableIDElements) {
            //mappableIDElements will be undefined if the form has not rendered yet
            for(var i=0;i<mappableIDElements.length;i++) {
                var formEl = mappableIDElements[i];
                if (formEl.type.toLowerCase() == "checkbox") {
                    //add hidden fields to the form for those mappableIDs
                    if (formEl.checked) {
                        this.addHiddenFieldForMappableID(formEl.value);
                    }
                }
            }
        }
    }

    this.afterSearchResultChange = function() {
        //first find all of those mappableIDs that represented as checkboxes in the form
        var mappableIDElements = this.form["listingID"] || this.form["mappableID"];
        var hiddenFieldsToRemove = new Array();
        for(var i=0;mappableIDElements && i<mappableIDElements.length;i++) {
            var formEl = mappableIDElements[i];
            if (formEl.type.toLowerCase() == "checkbox") {
                //if the value exists as a hidden field then remove it and check the checkbox
                var hidden = document.getElementById("hiddenMappableID"+formEl.value);
                if (hidden) {
                    hiddenFieldsToRemove.push(hidden);
                    formEl.checked = true;
                }
            }
        }
        for(var i=0;i<hiddenFieldsToRemove.length;i++) {
            //we remove the fields at the end so that we don't get out of order while iterating through the checkbox fields
            this.form.removeChild(hiddenFieldsToRemove[i]);
        }
    }

    this.isSelected = function(mappableID) {
        isSelected = false;
        //is there a hidden field
        if (document.getElementById("hiddenMappableID"+mappableID)) {
            isSelected = true;
        } else {
            var checkbox = document.getElementById("checkboxMappableID"+mappableID);
            if (checkbox && checkbox.checked) {
                isSelected = true;
            }
        }
        return isSelected;
    }
    this.addHiddenFieldForMappableID = function(mappableID,fieldName) {
        var id = "hiddenMappableID"+mappableID;
        fieldName = fieldName||"mappableID"
        var fieldElement = document.getElementById(id);
        if (fieldElement && fieldElement.name != fieldName) {
           //make sure that we can handle fields named listingID and mappableID
           this.removeHiddenFieldForMappableID(mappableID);
        }
        fieldElement = document.getElementById(id);
        if (!fieldElement) {
            var hidden = document.createElement("input");
            hidden.setAttribute("type","hidden");
            hidden.setAttribute("id",id);
            hidden.setAttribute("name",fieldName);
            hidden.setAttribute("value",mappableID);
            this.form.appendChild(hidden);
        }
    }

    this.removeHiddenFieldForMappableID = function(mappableID) {
        var hidden = document.getElementById("hiddenMappableID"+mappableID);
        if (hidden) {
            this.form.removeChild(hidden);
        }
    }

}
MappableIDFieldManager.instance = null;

function PaginationManager(mappableController,containerElement) {
    if (PaginationManager.instance) {
        return PaginationManager.instance;
    }
    PaginationManager.instance = this;

    this.mappableController = mappableController;
    this.containerElement = containerElement;
    this.currentPage = 1;

    this.getNumberOfPages = function() {
        return pages = Math.ceil(this.mappableController.visibleMappableIDsInOrder.length/MappableController.LISTINGS_PER_PAGE);
    }

    this.refresh = function() {
        if (this.mappableController) {
            var html = "Search<a href='javascript:search();' style='text-decoration:none'>&nbsp;</a>Results:&nbsp; ";
            var pages = this.getNumberOfPages();
            for (var i=1;i<=pages;i++) {
                html += "<a id=\"page"+i+"Link\" ";
                if (this.currentPage == i) {
                    html += " style=\"text-decoration: none;\" ";
                }
                html += " href='javascript:(new PaginationManager().goToPage("+i+"));'>"+i+"</a>&nbsp; ";
            }
            if (this.mappableController.visibleMappableIDsInOrder.length >= 500) {
                html += " (Top <strong>500</strong> "+new MappableManager().mappableType +" shown)";
            }
            if (this.containerElement) {
                this.containerElement.innerHTML = html;
            }
        }
    }

    this.goToPage = function(page) {
        var oldPageLink = document.getElementById("page" + this.currentPage + "Link");
        if (oldPageLink) {
            oldPageLink.style.textDecoration = "";
            this.currentPage = page;
            var newPageLink = document.getElementById("page" + this.currentPage + "Link");
            newPageLink.style.textDecoration = "none";
        }
        if (this.mappableController) {
            this.mappableController.mappableIDFieldManager.beforeSearchResultChange();
            this.mappableController.showSearchResultsForPage(page,new MappableManager().currentPage,function(){checkHandler(new MappableManager().mappableType.toLowerCase() + "ID")});
        }
    }
}
PaginationManager.instance = null;

/** Base search class */
function Search(className,searchByIDMethodName) {
    this.className = className;
    this.searchByIDMethodName = searchByIDMethodName;

    this.searchByIDs = function(ids, callback) {
        var results = new Array();
        var caller = null;
        var handler = function() {
            var req = caller.ajax.requestor;
            if (req.readyState == 4) {
                if (!req.status || req.status == 200) {
                    var xmlDoc = req.responseXML;
                    results = new XMLParser().parse(xmlDoc);
                } else {
                    window.status = "An error has occurred: " + req.statusText;
                }
                callback(results);
            }
        }
        if (ids && ids.length > 0) {
            caller = new AJAXMethodCaller(this.className,this.searchByIDMethodName,handler);
            caller.addParameter(AJAXMethodCaller.INTEGER_ARRAY_TYPE,ids);
            caller.call();
        } else {
            callback(new Array());
        }
    }

    this.searchByPage = function(page,callback) {
        var ids = new MappableController().getMappableIDsForPage(page);
        return this.searchByIDs(ids,callback);
    }

}

function SortableSearch(className,searchByIDMethodName,searchWithSortMethodName) {
    Search.call(this,className,searchByIDMethodName,searchWithSortMethodName);
    this.searchWithSortMethodName = searchWithSortMethodName;

    this.searchWithSort = function(ids, sortColumn, callback) {
        var results = new Array();
        var caller = null;
        var handler = function() {
            var req = caller.ajax.requestor;
            if (req.readyState == 4) {
                if (!req.status || req.status == 200) {
                    var xmlDoc = req.responseXML;
                    results = new XMLParser().parse(xmlDoc);
                } else {
                    window.status = "An error has occurred: " + req.statusText;
                }
                callback(results);
            }
        }

        var caller = new AJAXMethodCaller(this.className,this.searchWithSortMethodName,handler);
        caller.addParameter("com.catylist.ui.search.SearchSession","session:search.session.data");
        caller.addParameter("java.lang.String",sortColumn);
        if (!ids) {
            ids = new Array();
        }
        caller.addParameter(AJAXMethodCaller.INTEGER_ARRAY_TYPE,ids);
        caller.call();
    }
}

function ListingSearch() {
    SortableSearch.call(this,"com.catylist.ajax.MappedListingsDataProvider","populateFromIDs","populateLiteListingsBySort");
}

function AgentSearch() {
    Search.call(this,"com.catylist.ajax.MappedAgentsDataProvider","populateFromIDs");
}
function OrganizationSearch() {
    Search.call(this,"com.catylist.ajax.MappedOrganizationsDataProvider","populateFromIDs");
}
function MarketMapSearch() {
    Search.call(this,"com.catylist.ajax.MarketMapDataProvider","populateFromIDs");
}
function ComparableSearch() {
    SortableSearch.call(this,"com.catylist.ajax.MappedComparablesProvider","populateFromIDs","populateLiteComparablesBySort");
}
function PropertySearch() {
    SortableSearch.call(this,"com.catylist.ajax.MappedPropertiesProvider","populateFromIDs","populateLiteEntitiesBySort");
}

/** parses results of searches where the returned xml contains mappables */
function XMLParser() {
    if (XMLParser.instance) {
        return XMLParser.instance;
    }
    XMLParser.instance = this;

    this.parse = function(xmlDoc) {
        var mappables = new Array();
        var mappableElements = xmlDoc.documentElement.getElementsByTagName("mappable");

        for (var i = 0; i < mappableElements.length; i++) {
            var el = mappableElements[i];
            var id = el.getAttribute("id");
            var lat = el.getAttribute("latitude");
            var lon = el.getAttribute("longitude");
            var mappable = new Mappable(id,lon,lat);
            var searchResult = DomUtil.getChildNodesOfType(el,"searchResult")[0];
            mappable.searchResult = this.getTextFromNode(searchResult);
            var infoHTML = DomUtil.getChildNodesOfType(el,"infoWindow")[0];
            mappable.infoHTML = this.getTextFromNode(infoHTML,"Not available");
            mappables.push(mappable)
        }
        return mappables;
    }

    this.getTextFromNode = function(node,defaultText) {
        var nodeText = defaultText||"";
        for (var i=0;i<node.childNodes.length;i++) {
            var text = node.childNodes[i].nodeValue;
            if (text) {
                //trim
                text = text.replace(/^\s+|\s+$/, '')
            }
            if (text) {
                nodeText = text;
            }
        }
        return nodeText;
    }
}
XMLParser.instance = null;

function Polygon(map) {
    this.map = map;
    this.points = new Array();
    this.lines = new Array();
    this.line = null;
    this.beginPoint = null;
    this.closed = false;
    this.started = false;
    this.startMarker = null;

    this.start = function() {
        this.clear();
        this.started = true;
    }

    this.addPoint = function(gPoint) {
        if (this.started) {
            if (this.closed) {
                this.clear();
            }

            if (this.beginPoint && this.map) {
                var line = new GPolyline([this.beginPoint,gPoint],"#000099", 4);
                this.map.addOverlay(line);
                this.lines.push(line);
                if (this.startMarker) {
                    this.map.removeOverlay(this.startMarker);
                    this.startMarker = null;
                }
            } else if (this.map) {
                //draw the first point
                this.startMarker = new GMarker(gPoint,Polygon.startIcon);
                this.map.addOverlay(this.startMarker);
            }

            this.points.push(gPoint);
            this.beginPoint = gPoint;
        }
    }

    this.getSerializedPoints = function() {
        var pointsSerializedInArray = new Array();

        for (var i=0;i<this.points.length;i++) {
            var point = this.points[i];
            pointsSerializedInArray.push(point.x + ":" + point.y);
        }
        return pointsSerializedInArray.join(",");
    }

    this.close = function() {
        if (!this.closed) {
            //first plot the full line
            this.points.push(this.points[0]);
            if (this.map) {
                try {
                    this.line = new GPolyline(this.points,"#000099", 4);
                    this.map.addOverlay(this.line);
                    this.clearIntermediateLines();
                } catch (e) {
                }
            }
            //even if there is some googlemaps error, still specify that the polygon is closed
            this.closed = true;
            this.started = false;

            var stateManager = new StateManager();
            if (this == stateManager.polygon) {
                //only serialize if this is the managed polygon
                stateManager.serializePolygon();
            }
        }
    }

    this.clearIntermediateLines = function() {
        for (var i=0;i<this.lines.length;i++) {
            this.map.removeOverlay(this.lines[i]);
        }
        this.lines = new Array();
    }

    this.clear = function() {
        this.clearIntermediateLines();
        if (this.map && this.line){
            this.map.removeOverlay(this.line);
        }
        this.points = new Array();
        this.line = null;
        this.beginPoint = null;
        this.closed = false;
        var stateManager = new StateManager();
        if (this == stateManager.polygon) {
            //only serialize if this is the managed polugon
            stateManager.serializePolygon();
        }
    }

    this.isBeginState = function() {
        return !this.beginPoint;
    }

    this.containsPoint = function(testPoint) {
         //based on work in http://www.scottandrew.com/js/js_util.js
         var xnew,ynew,xold,yold,x1,y1,x2,y2,i;
         var inside=false;
        
         if (this.points.length >= 3 && testPoint) { // points don't describe a polygon, or there is no valid testPoint
            px = testPoint.x;
            py = testPoint.y;
            var lastPoint = this.points[this.points.length - 1];
            xold = lastPoint.x;
            yold = lastPoint.y;

            for (var i=0 ; i < this.points.length ; i++) {
                var point = this.points[i];
                xnew = point.x;
                ynew = point.y;
                if (xnew > xold) {
                    x1=xold;
                    x2=xnew;
                    y1=yold;
                    y2=ynew;
                }else {
                    x1=xnew;
                    x2=xold;
                    y1=ynew;
                    y2=yold;
                }
                if ((xnew < px) == (px <= xold) && ((py-y1)*(x2-x1) < (y2-y1)*(px-x1))) {
                    inside=!inside;
                }
                xold=xnew;
                yold=ynew;
            }
        }
        return inside;
    }

    this.zoomToFit = function() {
        var north = 0
        var south = 0
        var east = 0
        var west = 0;
        for (var i=0; i< this.points.length;i++) {
            var point = this.points[i];
            if (!north || point.y > north) {
                north = point.y;
            }
            if (!south || point.y < south) {
                south = point.y;
            }
            if (!east || point.x > east) {
                east = point.x;
            }
            if (!west || point.x < west) {
                west = point.x;
            }
        }
        this.map.zoomToFit(north,south,east,west);
    }
}
Polygon.startIcon = new GIcon();
Polygon.startIcon.image = "http://www.commercialiq.com/images/map/start.png";
Polygon.startIcon.iconSize = new GSize(4,4);
Polygon.startIcon.iconAnchor = new GPoint(2,2);

function CMapFactory(container, mapTypes, width, height) {
    var map = new GMap(container, mapTypes, width, height);
    //disable katrina map type since it causes errors in ie
    var katrinaType = window["_KATRINA_TYPE"]
    if (katrinaType && katrinaType.bounds) {
        katrinaType.bounds = new GBounds(0,0,0,0);
    }

    map.visibleOverlaysByName = new Array();
    map.addOverlayByName = function(name,marker) {
        name = new String(name);
        if (map.visibleOverlaysByName[name]) {
            map.removeOverlayByName(name);
        }
        if (marker) {
            map.addOverlay(marker);
            map.visibleOverlaysByName[name] = marker;
        }
    }
    map.getOverlayByName = function(name) {
        return map.visibleOverlaysByName[name];
    }
    map.replaceOverlayByName = function(name,newMarker) {
        if (map.visibleOverlaysByName[name] && newMarker) {
            map.removeOverlayByName(name);
            map.addOverlayByName(name,newMarker);
        }
    }
    map.removeOverlayByName = function(name) {
        var marker = map.getOverlayByName(name);
        map.visibleOverlaysByName[name] = null;
        if (marker) {
            map.removeOverlay(marker);
        }
    }
    map.toggleInfoWindowByName = function(name) {
        var marker = map.visibleOverlaysByName[name];
        if (marker) {
            if (map["openInfoWindowMarker"] && map.openInfoWindowMarker == marker) {
                map.closeInfoWindow();
                map.openInfoWindowMarker = null;
            } else {
                if (map["openInfoWindowMarker"] && map.openInfoWindowMarker != marker) {
                    map.closeInfoWindow();
                }
                marker.openInfoWindow();
            }
        }
    }
    map.zoomToFit = function(north,south,east,west,minZoom,maxZoom,buffered) {
        north  = parseFloat(north);
        south  = parseFloat(south);
        east  = parseFloat(east);
        west  = parseFloat(west);
        var midx = ((east - west)/2 + west);
        var midy = ((north - south)/2 + south);
        var center = new GPoint(midx,midy);
        var zoom;
        var pixels;
        var hSpan = (east - west);
        var vSpan = (north - south);
        if (hSpan >  vSpan) {
            zoom = hSpan;
            pixels = container.clientWidth;
        } else {
            zoom = vSpan;
            pixels = container.clientHeight;
        }

        var constant = 0.00001078; //degrees per pixel at zoom=0;
        zoom = zoom / (constant * pixels) / Math.cos(midy / 180 * Math.PI);
        zoom = Math.log(zoom) / Math.LN2;
        zoom = Math.ceil(zoom)

        zoom = (zoom < 1)?1:zoom;
        if (minZoom && zoom < minZoom) {
            zoom = minZoom;
        }
        if (maxZoom && zoom > maxZoom) {
            zoom = maxZoom;
        }
        //don't zoom if we have -9090909 in there, or if the center is exactly 0/0
        if ((midx > -181 && midy > -181) && (midx != 0 && midy != 0)) {
            map.centerAndZoom(center,zoom);
            if (! buffered) {
                //now add a buffer of 32 pixels on top and 16 to the east and west
                var span = map.getSpanLatLng();
                var degreesPerVerticalPixel = span.height/container.clientHeight;
                var degreesPerHorizontalPixel = span.width/container.clientWidth;
                north = north + (32 * degreesPerVerticalPixel);
                east = east + (16 * degreesPerHorizontalPixel);
                west = west - (16 * degreesPerHorizontalPixel);
                map.zoomToFit(north,south,east,west,minZoom,maxZoom,true);
            }
        }

    }

    map.isInHighResBounds = function() {
        var poly = new Polygon();
        poly.start();
        poly.addPoint(new GPoint(-139.0430,60.0429));
        poly.addPoint(new GPoint(-64.7843,60.8329));
        poly.addPoint(new GPoint(-65.2567,18.0721));
        poly.addPoint(new GPoint(-68.0383,17.8088));
        poly.addPoint(new GPoint(-80.4199,24.4071));
        poly.addPoint(new GPoint(-97.4268,25.6811));
        poly.addPoint(new GPoint(-124.1012,27.6835));
        poly.close();
        return poly.containsPoint(map.getCenterLatLng());

    }

    //prevent zooming higher than leven 8 outside of north america
    GEvent.addListener(map,"zoom", function(oldZoom,newZoom) {
        if (newZoom < 8 && !map.isInHighResBounds()) {
            map.zoomTo(8);
        }
    });

    return map;
}

/*
    stores information about the state of this map in the resultsForm
    enabling us to restore the data onload forcing recalcitrant browsers
    to correctly maintain state when clicking back
*/
function StateManager(mappableController,polygon,idsField,polygonField,pageField) {
    if (StateManager.instance) {
        return StateManager.instance;
    }
    StateManager.instance = this;
    this.mappableController = mappableController;
    this.polygon = polygon;
    this.idsField = idsField;
    this.polygonField = polygonField;
    this.pageField = pageField;
    this.serializePolygon = function() {
        var polygonPoints = new Array();
        for (var i=0;i<this.polygon.points.length;i++) {
            var point = this.polygon.points[i];
            polygonPoints.push(point.x + ":" + point.y);
        }
        this.polygonField.value = polygonPoints.join(",");
    }
    this.serializeIDs = function() {
        if (this.idsField) {
            var ids = mappableController.visibleMappableIDsInOrder;
            var serializedValues = new Array();
            for (var i=0;i<ids.length;i++) {
                var mappable = mappableController.mappableManager.getLiteMappable(ids[i]);
                serializedValues.push(new Array(mappable.id,mappable.point.x,mappable.point.y).join(":"));
            }
            this.idsField.value = serializedValues.join(",");
            this.pageField.value = mappableController.currentPage;
        }
    }
    /** calls callback once deserialized */
    this.deserialize = function(callback) {
        var wasDeserialized = false;

        if (this.idsField && this.idsField.value) {
            wasDeserialized = true;
            this.mappableController.clearAllVisibleMappables();
            var serializedMappables = this.idsField.value.split(",");
            for (var i=0;i<serializedMappables.length;i++) {
                var deserializedValues = serializedMappables[i].split(":");
                var mappable = new Mappable(deserializedValues[0],deserializedValues[1],deserializedValues[2]);
                this.mappableController.addMappable(mappable);
            }
            this.mappableController.indexMappables();
        }

        if (this.polygonField && this.polygonField.value) {
            wasDeserialized = true;
            var points = this.polygonField.value.split(",");
            this.polygon.start();
            for (var i=0;i<points.length;i++) {
                var coords = points[i].split(":");
                var x = parseFloat(coords[0]);
                var y = parseFloat(coords[1]);
                this.polygon.addPoint(new GPoint(x,y));
            }
            this.polygon.close();
        }

        if (this.pageField && this.pageField.value && (this.pageField.value != this.mappableController.currentPage || this.pageField.value == 1)) {
            wasDeserialized = true;
            var mgr = new PaginationManager();
            mgr.goToPage(parseInt(this.pageField.value));
            mgr.refresh();
        }

        //handle zoom
        if (this.polygonField && this.polygonField.value) {
            this.polygon.zoomToFit();
        } else if (this.idsField && this.idsField.value) {
            this.mappableController.zoomToFitMappables();
        }

        return wasDeserialized;

    }
}
StateManager.instance = null;

/**turn variable length arguments or an array into a single array */
function unpackArguments(args,startIndex) {
    startIndex = startIndex||0;
    var argumentsArray = new Array();
    if (isArray(args[startIndex])) {
        argumentsArray = args[startIndex];
    } else {
        //can't use Array().slice() with the implicit arguments array
        for (var i=startIndex;i<args.length;args++) {
            argumentsArray.push(args[i]);
        }
    }
    return argumentsArray;
}
/**supports checkall checkbox**/
function checkHandler(checkboxName) {
    var isAllChecked = true;
    var checkboxes = $('input[name='+checkboxName+']');
    if (checkboxes) {
        for (var i=0;i<checkboxes.length;i++) {
            var box = checkboxes[i];
            if (!box.checked) {
                isAllChecked = false;
                break;
            }
        }
        $("#checkall")[0].checked = isAllChecked;
    }
}
