
////////////////////////////////////////////////////////////
//
//  CMapMgr
//
//  this is a class to do the management of the map
//  should only be one instance of this class
//



function CMapMgr()
{


    ////////////////////////////////////////////////////////////////////////////////
    ////////////////////////////////////////////////////////////////////////////////

    ////////////////////////////////////////////////////////////
    //
    //  public attributes
    //

    //
    //  'loading' parameters

    this.load = new Object;
    this.load.loadLat = null;
    this.load.loadLng = null;
    this.load.loadZoom = null;
    this.load.bFirstConfigure = true;
    this.load.fnFirstInitCB = null;



    //
    //  general behaviour control params

    this.behaviour = new Object;
    this.behaviour.noProjectViewing = false;
    this.behaviour.bNoRegions = false;

    this.behaviour.nClusterTolerancePixelsX = 40;
    this.behaviour.nClusterTolerancePixelsY = 40;

    this.behaviour.dMinLatBounds = 0.025;
    this.behaviour.dMinLngBounds = 0.025;




    ////////////////////////////////////////////////////////////////////////////////
    ////////////////////////////////////////////////////////////////////////////////

    ////////////////////////////////////////////////////////////
    //
    //  private attributes
    //

    this._map = null;


    ////////////////////////////////////////////////////////
    //
    //  we have two different 'modes' for viewing in
    //
    //      Region - viewing a region and its projects
    //      Project - viewing a project and its stories
    //


    this._mode = "region";

    this._bDirty = false;


    //
    //  vars we maintain for "Region Viewing" mode

    this._modeRegion = new Object;
    this._modeRegion.nCurrentRegionId = 0;
    this._modeRegion.regionInfo = null;
    this._modeRegion.projectInfo = null;
    this._modeRegion.projectMarkers = new Array();


    //
    //  vars we maintain for "Project Viewing" mode

    this._modeProject = new Object;
    this._modeProject.nCurrentProjectId = 0;
    this._modeProject.projectInfo = null;
    this._modeProject.storyMarkers = new Array();


    //
    //  mouse tracking

    this._mouseTrack = new Object;
    this._mouseTrack.idleId = null;
    this._mouseTrack.idleTimeout = 350;
    this._mouseTrack.currLatLng = null;


    //
    //  map window size tracking

    this._sizeLastWid = null;
    this._sizeLastHgt = null;


    //
    //  misc timer ids

    this._timers = new Object;
    this._timers.nCheckResize = null;


    //
    //  project view dialog

    this._projectViewDlg = null;





    ////////////////////////////////////////////////////////////
    //
    //  methods
    //



    ////////////////////////////////////////////////////////////////////////////////
    ////////////////////////////////////////////////////////////////////////////////

    ////////////////////////////////////////////////////////////
    //
    //  main public interface
    //


    ////////////////////////////////////////////////////////////
    //
    //  loadMap()
    //
    //  called by page onload - creates map and starts load of data
    //
    //  this == CMapMgr
    //

    CMapMgr.prototype.loadMap =
        function(initcb)
        {
            var that = this;


            //
            //  first init callback support

            this.load.fnFirstInitCB = initcb;


            //
            //  create map

            this._map = new GMap2(document.getElementById("map"));


            //
            //  add standard map type and movement controls

            this._map.addControl(new GLargeMapControl(), new GControlPosition(G_ANCHOR_BOTTOM_LEFT, new GSize(10, 40)));
            this._map.addControl(new GMapTypeControl());
            this._map.addControl(new GScaleControl(), new GControlPosition(G_ANCHOR_TOP_RIGHT, new GSize(20, 35)));
            this._map.addControl(new GOverviewMapControl(new GSize(100, 100)), new GControlPosition(G_ANCHOR_BOTTOM_RIGHT, new GSize(0, 0)));
            //this._map.addControl(new google.maps.LocalSearch());

            this._map.enableScrollWheelZoom();
            this._map.enableDoubleClickZoom();
            this._map.enableGoogleBar();


            //
            //  position overview control

            setTimeout(
                function()
                {
                    var oMap = document.getElementById("map_overview");
                    if (!oMap)
                    {
                        return;
                    }

                    if (oMap.style)
                    {
                        oMap.style.right = "4px";
                        oMap.style.bottom = "4px";
                    }

                    oMap.firstChild.style.border = "1px solid gray";
                    oMap.firstChild.style.backgroundColor = "#aaaaaa";
                    oMap.firstChild.firstChild.style.left = "1px";
                    oMap.firstChild.firstChild.style.top = "1px";
                    oMap.firstChild.firstChild.style.width = "98px";
                    oMap.firstChild.firstChild.style.height = "98px";
                },
                2000);


            //
            //  keep checking window size we call GMap.checkResize() when needed

            this._timers.nCheckResize = setInterval(
                function() {
                    if (!that._map)
                    {
                        return;
                    }
                    var nMapHgt = gWebAppMgr.getMapDivHgt();
                    var nMapWid = gWebAppMgr.getMapDivWid();
                    if (!that._sizeLastWid || that._sizeLastWid != nMapWid ||
                            !that._sizeLastHgt || that._sizeLastHgt != nMapHgt)
                    {
                        that._sizeLastWid = nMapWid;
                        that._sizeLastHgt = nMapHgt;
                        that._map.checkResize();
                    }
                },
                2000);


            //
            //  project view dialog

            //this._projectViewDlg = new CProjectViewDlg('projectView');


            //
            //  init the map when it is loaded and
            //  load the data for the initial view

            this._initLoadedGmap();
            this._initiateDataLoad();
        };


    ////////////////////////////////////////////////////////////
    //
    //  checkResize()
    //
    //  call this method when the containing window changes size
    //

    CMapMgr.prototype.checkResize =
        function()
        {
            var that = this;
        };


    ////////////////////////////////////////////////////////////
    //
    //  getGMap()
    //
    //  returns the Google GMap object
    //

    CMapMgr.prototype.getGMap =
        function()
        {
            return this._map;
        };



    ////////////////////////////////////////////////////////////
    //
    //  refreshDataForCurrentView()
    //
    //  refreshes onscreen data, leaving current view intact
    //

    CMapMgr.prototype.refreshDataForCurrentView =
        function()
        {
            this._bDirty = true;

            if (this._isModeRegion())
            {
                this._modeRegion_RefreshDataForCurrentView();
            }
            else if (this._isModeProject())
            {
                this._modeProject_RefreshDataForCurrentView();
            }
        };



    ////////////////////////////////////////////////////////////////////////////////
    ////////////////////////////////////////////////////////////////////////////////

    ////////////////////////////////////////////////////////////
    //
    //  main private functions
    //

    ////////////////////////////////////////////////////////////
    //
    //  _initLoadedGmap()
    //
    //  do the init'ing stuff to the map that can only
    //  be done when the GMap is 'loaded'
    //

    CMapMgr.prototype._initLoadedGmap =
        function()
        {
            var that = this;

            var id = setInterval(
                        function()
                        {
                            //
                            //  keep trying until the Gmap object is properly loaded and centred

                            if (!that._map.isLoaded())
                            {
                                return;
                            }
                            clearInterval(id);


                            //
                            //  set map type from cookie

                            var mapType = that._map.getMapTypes();
                            var cookieMapType = CPsCookieMgr.get('googleMapType');

                            if (cookieMapType)
                            {
                                if (cookieMapType == 'Satellite')
                                {
                                    that._map.setMapType(mapType[1]);
                                }
                                else if (cookieMapType == 'Hybrid')
                                {
                                    that._map.setMapType(mapType[2]);
                                }
                                else
                                {
                                    that._map.setMapType(mapType[0]);
                                }
                            }


                            //
                            //  enable smooth zooming - doesn't work in current version (2.92)

                            that._map.enableContinuousZoom();


                            //
                            //  bind events

                            GEvent.bind(that._map, "click", that, that._handleGMap_click);
                            GEvent.bind(that._map, "maptypechanged", that, that._handleGMap_maptypechanged);
                            GEvent.bind(that._map, "zoomend", that, that._handleGMap_zoomchanged);
                        },
                        50);
        };




    ////////////////////////////////////////////////////////////
    //
    //  _initiateDataLoad()
    //
    //  call this routine to start loading data for the current
    //  view mode. the current region etc is also taken into
    //  account
    //

    CMapMgr.prototype._initiateDataLoad =
        function()
        {
            if (this._isModeRegion())
            {
                return this._modeRegion_InitiateDataLoad();
            }
            else
            {
                return this._modeProject_InitiateDataLoad();
            }
        };



    ////////////////////////////////////////////////////////////
    //
    //  _checkFirstConfigure()
    //
    //  call this routine after a map configure - if it is the
    //  first time the load-nominated callback will be called
    //

    CMapMgr.prototype._checkFirstConfigure =
        function()
        {
            if (this.load.bFirstConfigure)
            {
                if (this.load.fnFirstInitCB)
                {
                    this.load.fnFirstInitCB();
                }

                this.load.bFirstConfigure = false;
            }
        }



    ////////////////////////////////////////////////////////////
    //
    //  _handleGMap_click
    //

    CMapMgr.prototype._handleGMap_click =
        function(overlay, latlng)
        {
            if (!gWebAppMgr.debugRegionTrace)
            {
                return;
            }

            GLog.write(CPsText.sprintf("lat: %f lng: %f", latlng.lat(), latlng.lng()));
        };




    ////////////////////////////////////////////////////////////
    //
    //  _handleGMap_maptypechanged
    //

    CMapMgr.prototype._handleGMap_maptypechanged =
        function()
        {
            CPsCookieMgr.set('googleMapType', this._map.getCurrentMapType().getName(), '', '/', '', '' );
        };




    ////////////////////////////////////////////////////////////
    //
    //  _handleGMap_zoomchanged
    //

    CMapMgr.prototype._handleGMap_zoomchanged =
        function(nOldLevel, nNewLevel)
        {
            if (this._isModeRegion())
            {
                this._modeRegion_HandleGMap_zoomchanged(nOldLevel, nNewLevel);
            }
            else
            {
                this._modeProject_HandleGMap_zoomchanged(nOldLevel, nNewLevel);
            }
        };




    ////////////////////////////////////////////////////////////
    //
    //  breadcrumb support
    //

    CMapMgr.prototype._fillBreadcrumb =
        function()
        {
            var that = this;
            var $breadcrumb = jQuery('#breadcrumb');
            $breadcrumb.slideUp('normal', function() {
                var arrRegions;
                if (that._isModeRegion())
                {
                    arrRegions = that._getRegionSeq(that._modeRegion.nCurrentRegionId);
                }
                else
                {
                    arrRegions = that._getRegionSeq(that._modeProject.projectInfo.regionId);
                }

                var szHtml = "<ul class='horiz'>";
                var szClass = '';
                if (arrRegions)
                {
                    jQuery.each(arrRegions, function (i, oRegion) {
                        szHtml += CPsText.sprintf(
                                    '<li %s><a href="#" class="region">%s</a></li>',
                                    szClass,
                                    this.name);
                        szClass = 'class="region sep"';
                    });
                }
                if (that._isModeProject())
                {
                    szHtml += CPsText.sprintf(
                                '<li class="sep">' +
                                    '<a href="#" class="project">%s</a> ' +
                                    '<a href="%s" target="new" class="projpage">(project page)</a>' +
                                '</li>',
                                that._modeProject.projectInfo.title,
                                that._modeProject.projectInfo.urls.projectPage);
                }

                jQuery('#breadcrumb .mapMenuInner').html(szHtml + "</ul>");

                jQuery('#breadcrumb a.region').each(function(idx) {
                    jQuery(this).click(function() {
                        that._gotoRegion(arrRegions[idx].id);
                    });
                });
                jQuery('#breadcrumb a.project').click(function() {
                    that._modeProject_PositionMap();
                });

                $breadcrumb.slideDown('normal');
            });
        };


    CMapMgr.prototype._getRegionSeq =
        function(nRegionId, arrRegions, arrChildren)
        {
            var that = this;

            if (!arrRegions)
            {
                var region = this._modeRegion.regionInfo;
                arrRegions = Array(region);

                return region.id == nRegionId ? arrRegions : this._getRegionSeq(nRegionId, arrRegions, region.children);
            }

            if (!arrChildren)
            {
                return false;
            }

            var bGotit = false;
            jQuery(arrChildren, function (i, oChildRegion) {
                var arrSave = arrRegions;
                arrRegions[arrRegions.length] = this;

                if (this.id == nRegionId || that._getRegionSeq(nRegionId, arrRegions, this.children))
                {
                    bGotit = true;
                    return false;
                }
                arrRegions = arrSave;
                return true;
            });

            return bGotit ? arrRegions : false;
        };


    CMapMgr.prototype._findRegion =
        function(nRegionId, oRegion)
        {
            if (!oRegion)
            {
                oRegion = this._modeRegion.regionInfo;
            }
            if (!oRegion)
            {
                return false;
            }
            if (oRegion.id == nRegionId)
            {
                return oRegion;
            }

            for (var i=0; i<oRegion.children.length; i++)
            {
                var oTryRegion = this._findRegion(oRegion.children[i]);
                if (oTryRegion)
                {
                    return oTryRegion;
                }
            }

            return false;
        };




    ////////////////////////////////////////////////////////////
    //
    //  _modeRegion_GotoRegion(nRegionId)
    //
    //  load the data for a new region
    //

    CMapMgr.prototype._gotoRegion =
        function(nRegionId)
        {
            if (!this._modeRegion.regionInfo)
            {
                return;
            }


            //
            //  clear away current markers

            this._mode = "region";
            this._modeRegion_ClearProjectMarkers();
            this._modeProject_ClearStoryMarkers();


            //
            //  set starting location
            //  i wish we could do a google-earth-like zoom, but continuous zoom doesn't
            //  seem to work - oh well

            var oRegion = this._findRegion(nRegionId);
            if (!oRegion)
            {
                return;
            }

            this._modeRegion.nCurrentRegionId = nRegionId;
            this._modeRegion.oCurrentRegion = oRegion;

            var lat = oRegion.bounds.midlat;
            var lng = oRegion.bounds.midlng;
            var nZoom = this._map.getBoundsZoomLevel(oRegion.bounds.latlngbounds);
            var centerPos = new GLatLng(parseFloat(lat), parseFloat(lng));

            if (this._map.isLoaded())
            {
                //  TODO-continuous zoom not working! DAMN!
                //this._map.setZoom(parseFloat(nZoom));
                //this._map.panTo(centerPos);

                this._map.setCenter(centerPos, parseFloat(nZoom));
            }
            else
            {
                this._map.setCenter(centerPos, parseFloat(nZoom));
            }

            if (this._bDirty)
            {
                this._modeRegion.projectMarkers = new Array();
                this._bDirty = false;
            }
            this._modeRegion_CreateProjectMarkers(nZoom);
            this._modeRegion_ShowProjectMarkers(nZoom);
            this._fillBreadcrumb();
        };



    ////////////////////////////////////////////////////////////
    //
    //  _setDefaultRegionView(oMode)
    //
    //  centres the map according to the bounds info of the
    //  main region in oMode
    //

    CMapMgr.prototype._setDefaultRegionView =
        function()
        {
            if (!this._modeRegion.regionInfo)
            {
                return;
            }

            this._gotoRegion(this._modeRegion.regionInfo.id);
        };



    ////////////////////////////////////////////////////////////
    //
    //  _showStory()
    //

    CMapMgr.prototype._showStory =
        function(oStory, nProjectId, data)
        {
            if (!oStory)
            {
                return;
            }

            var that = this;

            if (!data)
            {
                var szUrl = '/services/story_embed_html' +
								'?storyId=' + oStory.id +
								'&w=400' +
								'&p=' + nProjectId +
								"&t=" + (new Date().getTime());

                jQuery.ajax({
                    url:        szUrl,
                    success:    function(data) {
                                    that._showStory(oStory, nProjectId, data);
                                },
                    error:      function(XMLHttpRequest, textStatus, errorThrown) {
                                    alert('Error retrieving story data: ' + textStatus);
                                }
                });
            }
            else
            {
                jQuery('#story-panel .inner').html(data);

                jQuery('#story-panel .close').click(function() {
                    jQuery('#story-panel').jqmHide();
                });

                jQuery('#story-panel').jqm(
                    {
                        modal:      true,
                        onShow:     function(h) {
                                        h.w.fadeIn(400);
                                    },
                        onHide:     function(h) {
                                        h.o.fadeOut(200);
                                        h.w.fadeOut(400, function() {
                                            jQuery('#story-panel .inner').html('');
                                        });
                                    },
                        overlay:    50
                    }
                ).jqmShow();
            }
        };


    ////////////////////////////////////////////////////////////////////////////////
    ////////////////////////////////////////////////////////////////////////////////

    ////////////////////////////////////////////////////////////
    //
    //  mode support
    //

    CMapMgr.prototype._isModeRegion = function()
        {
            return this._mode == "region";
        };

    CMapMgr.prototype._isModeProject = function()
        {
            return this._mode == "project";
        };



    ////////////////////////////////////////////////////////////////////////////////
    ////////////////////////////////////////////////////////////////////////////////

    ////////////////////////////////////////////////////////////
    //
    //  mode "region" support.........
    //


    ////////////////////////////////////////////////////////////
    //
    //  _modeRegion_GmapHandleZoomChanged
    //

    CMapMgr.prototype._modeRegion_HandleGMap_zoomchanged =
        function(nOldLevel, nNewLevel)
        {
            //
            //  we have to clear project markers, calculate clusters
            //  for new zoom level, then display those markers

            //debugger;
            this._modeRegion_ClearProjectMarkers(nOldLevel);
            this._modeRegion_CreateProjectMarkers(nNewLevel);
            this._modeRegion_ShowProjectMarkers(nNewLevel);
        };




    ////////////////////////////////////////////////////////////
    //
    //  _modeRegion_RefreshDataForCurrentView()
    //
    //  refreshes data as though user credentials have changed
    //

    CMapMgr.prototype._modeRegion_RefreshDataForCurrentView =
        function()
        {
            this._modeRegion_InitiateDataLoadProjects();
        };


    ////////////////////////////////////////////////////////////
    //
    //  _modeRegion_InitiateDataLoad()
    //
    //  start the process of getting the JSON data for the given
    //  region by calling the server-side services
    //
    //  we have to get:
    //
    //      1. all regions and their boundaries
    //      2. all projects
    //
    //  we obtain this data synchronously, so kick off with the
    //  region info, after which project info is loaded
    //

    CMapMgr.prototype._modeRegion_InitiateDataLoad =
        function()
        {
            this._modeRegion_InitiateDataLoadRegion();
        };


    ////////////////////////////////////////////////////////////
    //
    //  _modeRegion_InitiateDataLoadRegion(nRegionId)
    //
    //  start the process of getting the XML data for the given
    //  region by calling the server-side service
    //

    CMapMgr.prototype._modeRegion_InitiateDataLoadRegion =
        function()
        {
            var that = this;

            var szUrl = DocPaths['RegionData'] + 'all_region_bounds.json';
            szUrl += "?t=" + (new Date().getTime());

            gWebAppMgr.setLoadingMsg("retrieving region data");

            if (gWebAppMgr.debug)
            {
                alert('region json: ' + szUrl);
            }

            jQuery.ajax({
                url:        szUrl,
                dataType:   'json',
                success:    function(oJSON) {
                                if (gWebAppMgr.debug)
                                {
                                    alert('here1');
                                }
                                gWebAppMgr.clearLoadingMsg();
                                if (oJSON.result != 'success')
                                {
                                    alert('Error retrieving region data(1): ' + oJSON.reason);
                                    return;
                                }
                                that._modeRegion_ConfigureRegions(oJSON);
                                that._modeRegion_InitiateDataLoadProjects();
                            },
                error:      function(XMLHttpRequest, textStatus, errorThrown) {
                                if (gWebAppMgr.debug)
                                {
                                    alert('here2');
                                }
                                gWebAppMgr.clearLoadingMsg();
                                alert('Error retrieving region data(2): ' + textStatus);
                            }
            });
        };



    ////////////////////////////////////////////////////////////
    //
    //  _modeRegion_ConfigureRegions(oJSON)
    //
    //  takes a json object which represents the data for all regions
    //

    CMapMgr.prototype._modeRegion_ConfigureRegions =
        function(json)
        {
            if (gWebAppMgr.debug)
            {
                alert('configure regions');
            }

            this._modeRegion.regionInfo = json.region;
            this._modeRegion_InitRegionInfo(this._modeRegion.regionInfo);
            this._modeRegion.nCurrentRegionId = this._modeRegion.regionInfo.id;
            this._modeRegion.oCurrentRegion = this._modeRegion.regionInfo;
        };


    ////////////////////////////////////////////////////////////
    //
    //  _modeRegion_InitRegionInfo(region)
    //
    //  initialises some region info
    //

    CMapMgr.prototype._modeRegion_InitRegionInfo =
        function(oRegion)
        {
            //
            //  have to set bounds stuff for each region

            oRegion.bounds.latlngbounds = new GLatLngBounds(
                                                    new GLatLng(oRegion.bounds.swlat, oRegion.bounds.swlng),
                                                    new GLatLng(oRegion.bounds.nelat, oRegion.bounds.nelng));
            //TODO-potential dateline bug!
            oRegion.bounds.midlat = oRegion.bounds.swlat + (oRegion.bounds.nelat-oRegion.bounds.swlat)/2;
            oRegion.bounds.midlng = oRegion.bounds.swlng + (oRegion.bounds.nelng-oRegion.bounds.swlng)/2;


            //
            //  recurse thru the kiddies

            for (var i=0; i<oRegion.children.length; i++)
            {
                this._modeRegion_InitRegionInfo(oRegion.children[i]);
            }
        };


    ////////////////////////////////////////////////////////////
    //
    //  _modeRegion_InitiateDataLoadProjects()
    //
    //  start the process of getting the JOSN data for the projects
    //  of the licence which are visible to the current user

    CMapMgr.prototype._modeRegion_InitiateDataLoadProjects =
        function()
        {
            var that = this;

            gWebAppMgr.setLoadingMsg("retrieving project data");

            var szUrl = '/services/get_vis_projects?';
            if (gPsLoginMgr.getUserToken())
            {
                szUrl += "&pstoken=" + gPsLoginMgr.getUserToken();
            }
            szUrl += "&attrProject=id|title|caption|creationDate|creatorInfo|location|urls|storyCount|regionInfo";
            szUrl += "&attrUser=id|name";
            szUrl += "&format=json";
            szUrl += "&map=1";
            szUrl += "&t=" + (new Date().getTime());

            if (gWebAppMgr.debug)
            {
                alert('projects: ' + szUrl);
            }

            jQuery.ajax({
                url:        szUrl,
                dataType:   'json',
                success:    function(oJSON) {
                                if (oJSON.result != 'success')
                                {
                                    alert('Error retrieving project data: ' + oJSON.reason);
                                    return;
                                }
                                gWebAppMgr.clearLoadingMsg();
                                that._modeRegion_ConfigureProjects(oJSON);
                            },
                error:      function(XMLHttpRequest, textStatus, errorThrown) {
                                gWebAppMgr.clearLoadingMsg();
                                alert('Error retrieving project data: ' + textStatus);
                                that._setDefaultRegionView();
                            }
            });
        };



    ////////////////////////////////////////////////////////////
    //
    //  _modeRegion_ConfigureProjects(oJSON)
    //

    CMapMgr.prototype._modeRegion_ConfigureProjects =
        function(oJSON)
        {
            this._modeRegion.projectInfo = oJSON.projects;
            gPsAppMgr.debugAlert(1, '#projects: ' + this._modeRegion.projectInfo.length);

            //debugger;
            this._setDefaultRegionView();

            this._checkFirstConfigure();
        };



    ////////////////////////////////////////////////////////////
    //
    //  _modeRegion_CreateProjectMarkers()
    //
    //  creates the project markers for the map at the current
    //  zoom level, taking into account clustering
    //

    CMapMgr.prototype._modeRegion_CreateProjectMarkers =
        function(nZoom)
        {
            var that = this;

            this._createClusteredMarkers(
                    nZoom,
                    this._modeRegion.projectMarkers,
                    this._modeRegion.projectInfo,
                    hdlrMouseover,
                    hdlrMouseout,
                    hdlrClick,
                    "nodeproject2_15x15.png",
                    "nodeproject2_cluster_22x20.png");

            gPsAppMgr.debugAlert(1, 'createprojmarkers: %d, %d',
                    this._modeRegion.projectMarkers[this._map.getZoom()] ?
                        this._modeRegion.projectMarkers[this._map.getZoom()].length : 0,
                    this._modeRegion.projectInfo.length);

            function hdlrMouseover()
            {
                var nNumProjects = this.arrInfo.length;
                var szText;
                if (nNumProjects == 1)
                {
                    szText = this.oFirstInfo.title;
                }
                else
                {
                    var szText = CPsText.sprintf('%d Projects', nNumProjects);
                }

                that._displaySimplePopup(
                        '#projectPopup',
                        szText,
                        new GLatLng(this.oFirstInfo.location.lat, this.oFirstInfo.location.lng));
            };

            function hdlrMouseout()
            {
                jQuery('#projectPopup').fadeOut('fast');
            };

            function hdlrClick()
            {
                jQuery('#projectPopup').fadeOut('fast');
                if (this.bSingle)
                {
                    //debugger;
                    that._modeProject_GotoProject(this.oFirstInfo.id);
                    //CPsWindowMgr.goToURL('document', this.oFirstInfo.urls.projectPage);
                }
                else
                {
                    that._map.setCenter(this.oPoint, gMapMgr._map.getZoom()+1);
                }
            };
        };


    ////////////////////////////////////////////////////////////
    //
    //  _modeRegion_ShowProjectMarkers(nZoom)
    //

    CMapMgr.prototype._modeRegion_ShowProjectMarkers =
        function(nZoom)
        {
            this._showMarkers(nZoom, this._modeRegion.projectMarkers);
        };


    ////////////////////////////////////////////////////////////
    //
    //  _modeRegion_ClearProjectMarkers(nZoom)
    //

    CMapMgr.prototype._modeRegion_ClearProjectMarkers =
        function(nZoom)
        {
            gPsAppMgr.debugAlert(1, 'clearprojmarkers: %d, %d',
                    this._modeRegion.projectMarkers[this._map.getZoom()] ?
                        this._modeRegion.projectMarkers[this._map.getZoom()].length : 0,
                    this._modeRegion.projectInfo.length);

            for (var i=0; i<20; i++)
            {
                this._clearMarkers(i, this._modeRegion.projectMarkers);
                this._modeRegion.projectMarkers[i] = null;
            }
            //this._clearMarkers(nZoom, this._modeRegion.projectMarkers);
        };


    ////////////////////////////////////////////////////////////
    //
    //  _modeRegion_LogProjectClusters()
    //

    CMapMgr.prototype._modeRegion_LogProjectClusters =
        function(nLevel)
        {
            gPsAppMgr.debugGLog(nLevel, 'dumping project clusters');
            jQuery.each(this._modeRegion.projectMarkers, function(i, arrClusters) {
                if (!arrClusters)
                {
                    gPsAppMgr.debugGLog(nLevel, 'zoom level %d: no clusters', i);
                }
                else
                {
                    gPsAppMgr.debugGLog(nLevel, 'zoom level %d: %d clusters', i, arrClusters.length);
                    jQuery.each(arrClusters, function(j, oCluster) {
                        gPsAppMgr.debugGLog(nLevel, 'cluster %d: %d projects', j, oCluster.arrInfo.length);
                        jQuery.each(oCluster.arrInfo, function(k, oProject) {
                            gPsAppMgr.debugGLog(nLevel, 'project "%s"', oProject.title);
                        });
                    });
                }
            });
        };





    ////////////////////////////////////////////////////////////////////////////////
    ////////////////////////////////////////////////////////////////////////////////

    ////////////////////////////////////////////////////////////
    //
    //  mode "project" support.........
    //


    ////////////////////////////////////////////////////////////
    //
    //  _modeProject_GmapHandleZoomChanged
    //

    CMapMgr.prototype._modeProject_HandleGMap_zoomchanged =
        function(nOldLevel, nNewLevel)
        {
            //
            //  we have to clear project markers, calculate clusters
            //  for new zoom level, then display those markers

            //debugger;
            this._modeProject_ClearStoryMarkers(nOldLevel);
            this._modeProject_CreateStoryMarkers(nNewLevel);
            this._modeProject_ShowStoryMarkers(nNewLevel);
        };




    ////////////////////////////////////////////////////////////
    //
    //  _modeProject_RefreshDataForCurrentView()
    //
    //  refreshes data as though user credentials have changed
    //

    CMapMgr.prototype._modeProject_RefreshDataForCurrentView =
        function()
        {
            this._modeProject_InitiateDataLoad();
        };


    ////////////////////////////////////////////////////////////
    //
    //  _modeProject_GotoProject(nProjectId)
    //

    CMapMgr.prototype._modeProject_GotoProject =
        function(nProjectId)
        {
            this._mode = "project";
            this._modeRegion_ClearProjectMarkers();
            this._modeProject_ClearStoryMarkers();
            this._modeProject.nCurrentProjectId = nProjectId;
            this._modeProject_InitiateDataLoad();
        };


    ////////////////////////////////////////////////////////////
    //
    //  _modeProject_InitiateDataLoad()
    //
    //  start the process of getting the JSON data for the current
    //  project and all its visible stories

    CMapMgr.prototype._modeProject_InitiateDataLoad =
        function()
        {
            var that = this;

            gWebAppMgr.setLoadingMsg("retrieving project data");

            var szUrl = '/services/get_vis_project_data?';
            if (gPsLoginMgr.getUserToken())
            {
                szUrl += "&pstoken=" + gPsLoginMgr.getUserToken();
            }
            szUrl += "&pid=" + this._modeProject.nCurrentProjectId;
            szUrl += "&attrProject=id|title|caption|creationDate|creatorInfo|urls|stories|region|location";
            szUrl += "&attrStory=id|title|location|creatorInfo|creationDate|urls";
            szUrl += "&attrUser=id|name";
            szUrl += "&format=json";
            szUrl += "&t=" + (new Date().getTime());

            gPsAppMgr.debugAlert(1, 'project: %s', szUrl);

            jQuery.ajax({
                url:        szUrl,
                dataType:   'json',
                success:    function(oJSON) {
                                if (oJSON.result != 'success')
                                {
                                    if (oJSON.errorCode == gWebAppMgr.ERROR_NO_PERMISSION)
                                    {
                                        alert("You don't have permission to view this project. The map will revert to the top-level region.");
                                        that._modeRegion_RefreshDataForCurrentView();
                                    }
                                    else
                                    {
                                        alert('Error retrieving project data: ' + oJSON.reason);
                                    }
                                    return;
                                }
                                gWebAppMgr.clearLoadingMsg();
                                that._modeProject_ConfigureProject(oJSON);
                            },
                error:      function(XMLHttpRequest, textStatus, errorThrown) {
                                gWebAppMgr.clearLoadingMsg();
                                alert('Error retrieving project data: ' + textStatus);
                            }
            });
        };



    ////////////////////////////////////////////////////////////
    //
    //  _modeProject_ConfigureProject(oJSON)
    //

    CMapMgr.prototype._modeProject_ConfigureProject =
        function(oJSON)
        {
            //debugger;

            //
            //  clear current overlays and markers

            this._modeProject_ClearStoryMarkers();


            //
            //  clear old data

            if (this._modeProject.projectInfo)
            {
                delete this._modeProject.projectInfo;
            }
            this._modeProject.projectInfo = oJSON.project;
            this._modeProject_InitInfo();


            //
            //  move map so that all stories can be seen

            this._modeProject_PositionMap();
            this._fillBreadcrumb();


            //
            //  configure markers for stories

            if (this._bDirty)
            {
                this._modeProject.storyMarkers = new Array();
                this._bDirty = false;
            }
            this._modeProject_CreateStoryMarkers();
            this._modeProject_ShowStoryMarkers();


            //
            //  check for first configure

            this._checkFirstConfigure();
        };



    ////////////////////////////////////////////////////////////
    //
    //  _modeProject_InitInfo()
    //
    //  initialises project info
    //

    CMapMgr.prototype._modeProject_InitInfo =
        function()
        {
            var oInfo = this._modeProject.projectInfo;

            //
            //  have to set bounds stuff for each region

            var bnds = oInfo.bounds = new Object;
            bnds.nelat = gPsMapMgr.LAT_NULL;
            bnds.nelng = gPsMapMgr.LNG_NULL;
            bnds.swlat = gPsMapMgr.LAT_NULL;
            bnds.swlng = gPsMapMgr.LNG_NULL;

            //
            //  TODO- potential dateline bug!

            if (oInfo.stories)
            {
                jQuery.each(oInfo.stories, function(i, oStory) {
                    var loc = this.location;
                    if (loc.lat == gPsMapMgr.LAT_NULL)
                    {
                        return true;
                    }

                    if (bnds.nelat == gPsMapMgr.LAT_NULL || bnds.nelat < loc.lat)
                    {
                        bnds.nelat = loc.lat;
                    }
                    if (bnds.nelng == gPsMapMgr.LAT_NULL || bnds.nelng < loc.lng)
                    {
                        bnds.nelng = loc.lng;
                    }
                    if (bnds.swlat == gPsMapMgr.LAT_NULL || bnds.swlat > loc.lat)
                    {
                        bnds.swlat = loc.lat;
                    }
                    if (bnds.swlng == gPsMapMgr.LAT_NULL || bnds.swlng > loc.lng)
                    {
                        bnds.swlng = loc.lng;
                    }

                    return true;
                });
            }


            //
            //  adjust so that it is at least a minimum size

            if (bnds.nelat == gPsMapMgr.LAT_NULL)
            {
                bnds.nelat = oInfo.location.lat;
                bnds.nelng = oInfo.location.lng;
                bnds.swlat = oInfo.location.lat;
                bnds.swlng = oInfo.location.lng;
            }

            var diff = this.behaviour.dMinLatBounds - (bnds.nelat - bnds.swlat);
            if (diff > 0)
            {
                bnds.nelat += diff/2;
                bnds.swlat -= diff/2;
            }

            diff = this.behaviour.dMinLngBounds - (bnds.nelng - bnds.swlng);
            if (diff > 0)
            {
                bnds.nelng += diff/2;
                bnds.swlng -= diff/2;
            }

            bnds.latlngbounds = new GLatLngBounds(
                                        new GLatLng(bnds.swlat, bnds.swlng),
                                        new GLatLng(bnds.nelat, bnds.nelng));

            //TODO-potential dateline bug!
            bnds.midlat = bnds.swlat + (bnds.nelat-bnds.swlat)/2;
            bnds.midlng = bnds.swlng + (bnds.nelng-bnds.swlng)/2;
        };


    ////////////////////////////////////////////////////////////
    //
    //  _modeProject_CreateStoryMarkers()
    //
    //  creates the story markers for the map at the current
    //  zoom level, taking into account clustering
    //

    CMapMgr.prototype._modeProject_PositionMap =
        function()
        {
            var lat;
            var lng;
            var oBounds = this._modeProject.projectInfo.bounds;
            var nZoom;

            if (oBounds)
            {
                lat = oBounds.midlat;
                lng = oBounds.midlng;
                nZoom = parseFloat(this._map.getBoundsZoomLevel(oBounds.latlngbounds));
            }
            if (lat == undefined)
            {
                return;
            }

            var centerPos = new GLatLng(parseFloat(lat), parseFloat(lng));

            if (this._map.isLoaded())
            {
                //  TODO-continuous zoom not working! DAMN!
                //this._map.setZoom(nZoom);
                //this._map.panTo(centerPos);

                this._map.setCenter(centerPos, nZoom);
            }
            else
            {
                this._map.setCenter(centerPos, nZoom);
            }
        };



    ////////////////////////////////////////////////////////////
    //
    //  _modeProject_CreateStoryMarkers()
    //
    //  creates the story markers for the map at the current
    //  zoom level, taking into account clustering
    //

    CMapMgr.prototype._modeProject_CreateStoryMarkers =
        function(nZoom)
        {
            var that = this;

            this._createClusteredMarkers(
                    nZoom,
                    this._modeProject.storyMarkers,
                    this._modeProject.projectInfo.stories,
                    hdlrMouseover,
                    hdlrMouseout,
                    hdlrClick,
                    "nodestory2_15x15.png",
                    "nodeproject2_cluster_22x20.png");

            function hdlrMouseover()
            {
                var nNumStories = this.arrInfo.length;
                var szText;
                if (nNumStories == 1)
                {
                    szText = this.oFirstInfo.title;
                }
                else
                {
                    var szText = CPsText.sprintf('%d Stories', nNumStories);
                }

                that._displaySimplePopup(
                        '#storyPopup',
                        szText,
                        new GLatLng(this.oFirstInfo.location.lat, this.oFirstInfo.location.lng));
            };

            function hdlrMouseout()
            {
                jQuery('#storyPopup').fadeOut('fast');
            };

            function hdlrClick()
            {
                jQuery('#storyPopup').fadeOut('fast');
                if (this.bSingle)
                {
                    //CPsWindowMgr.goToURL('document', this.oFirstInfo.urls.storyPage);
                    that._showStory(this.oFirstInfo, that._modeProject.projectInfo.id);
                }
                else
                {
                    that._map.setCenter(this.oPoint, gMapMgr._map.getZoom()+1);
                }
            };
        };


    ////////////////////////////////////////////////////////////
    //
    //  _modeProject_ShowStoryMarkers(nZoom)
    //

    CMapMgr.prototype._modeProject_ShowStoryMarkers =
        function(nZoom)
        {
            this._showMarkers(nZoom, this._modeProject.storyMarkers);
        };


    ////////////////////////////////////////////////////////////
    //
    //  _modeProject_ClearStoryMarkers(nZoom)
    //

    CMapMgr.prototype._modeProject_ClearStoryMarkers =
        function(nZoom)
        {
            for (var i=0; i<20; i++)
            {
                this._clearMarkers(i, this._modeProject.storyMarkers);
                this._modeProject.storyMarkers[i] = null;
            }
            //this._clearMarkers(nZoom, this._modeProject.storyMarkers);
        };


    ////////////////////////////////////////////////////////////
    //
    //  _modeProject_LogStoryClusters()
    //

    CMapMgr.prototype._modeProject_LogStoryClusters =
        function(nLevel)
        {
            gPsAppMgr.debugGLog(nLevel, 'dumping story clusters');
            jQuery.each(this._modeProject.storyMarkers, function(i, arrClusters) {
                if (!arrClusters)
                {
                    gPsAppMgr.debugGLog(nLevel, 'zoom level %d: no clusters', i);
                }
                else
                {
                    gPsAppMgr.debugGLog(nLevel, 'zoom level %d: %d clusters', i, arrClusters.length);
                    jQuery.each(arrClusters, function(j, oCluster) {
                        gPsAppMgr.debugGLog(nLevel, 'cluster %d: %d stories', j, oCluster.arrInfo.length);
                        jQuery.each(oCluster.arrInfo, function(k, oStory) {
                            gPsAppMgr.debugGLog(nLevel, 'story "%s"', oStory.title);
                        });
                    });
                }
            });
        };




    ////////////////////////////////////////////////////////////////////////////////
    ////////////////////////////////////////////////////////////////////////////////

    ////////////////////////////////////////////////////////////
    //
    //  misc helpers
    //


    CMapMgr.prototype._createClusteredMarkers =
        function(nZoom, arrMarkers, arrInfo, fnHdlrMouseover, fnHdlrMouseout, fnHdlrClick, strSingleImage, strMultiImage)
        {
            if (!this._map)
            {
                return;
            }
            if (!arrInfo)
            {
                return;
            }


            var that = this;

            if (typeof nZoom == 'undefined')
            {
                nZoom = this._map.getZoom();
            }
            if (arrMarkers[nZoom])
            {
                //  already worked out markers for this zoom level
                return;
            }

            arrMarkers[nZoom] = new Array();
            var arrClusters = arrMarkers[nZoom];
            var oLatLngBounds = this._map.getBounds().toSpan();
            var oPixelBounds = this._map.getSize();
            var dToleranceLng = oLatLngBounds.lng() * this.behaviour.nClusterTolerancePixelsX / oPixelBounds.width;
            var dToleranceLat = oLatLngBounds.lat() * this.behaviour.nClusterTolerancePixelsY / oPixelBounds.height;

            gPsAppMgr.debugGLog(
                        2,
                        'latlngb: %.3f, %.3f  pixelb: %d, %d  tollatlng: %.3f, %.3f',
                        oLatLngBounds.lng(),
                        oLatLngBounds.lat(),
                        oPixelBounds.width,
                        oPixelBounds.height,
                        dToleranceLng,
                        dToleranceLat);


            //
            //  process each project in turn

            jQuery.each(arrInfo, function(i, oInfo) {

                //
                //  check for 'no position'

                if (oInfo.location.lat == gPsMapMgr.LAT_NULL || 
                        oInfo.location.lng == gPsMapMgr.LNG_NULL)
                {
                    return true;
                }


                //
                //  for oInfo, find cluster it belongs to - if none, then
                //  create a new cluster for it

                var oFoundCluster = null;
                jQuery.each(arrClusters, function(j, oCluster) {

                    //
                    //  see if oInfo is 'close enough' to any of the projects in oCluster.arrInfo

                    jQuery.each(oCluster.arrInfo, function(k, oClusterInfo) {

                        gPsAppMgr.debugGLog(
                            2,
                            'cmp:: %.3f %.3f  --  %.3f %.3f',
                            Math.abs(oInfo.location.lng - oClusterInfo.location.lng),
                            dToleranceLng,
                            Math.abs(oInfo.location.lat - oClusterInfo.location.lat),
                            dToleranceLat);

                        if (Math.abs(oInfo.location.lng - oClusterInfo.location.lng) <= dToleranceLng &&
                            Math.abs(oInfo.location.lat - oClusterInfo.location.lat) <= dToleranceLat)
                        {
                            oFoundCluster = oCluster;
                            return false;   // break .each loop
                        }
                    });

                    if (oFoundCluster)
                    {
                        return false;   // break .each loop
                    }
                });

                if (oFoundCluster)
                {
                    oFoundCluster.arrInfo[oFoundCluster.arrInfo.length] = oInfo;
                }
                else
                {
                    var oNewCluster = new Object;
                    oNewCluster.oGMarker = null;
                    oNewCluster.arrInfo = new Array(oInfo);
                    arrClusters[arrClusters.length] = oNewCluster;
                }

                return true;
            });


            //
            //  ok, now we have the clusters, go thru and create the markers

            jQuery.each(arrClusters, function(i, oCluster) {

                if (!this.arrInfo)
                {
                    return true;
                }

                oCluster.bSingle = this.arrInfo.length == 1;
                oCluster.oFirstInfo = this.arrInfo[0];
                oCluster.oPoint = new GLatLng(parseFloat(oCluster.oFirstInfo.location.lat), parseFloat(oCluster.oFirstInfo.location.lng));
                var oIcon = new GIcon();                
                oIcon.image = CPsText.sprintf('%simages/%s', DocPaths['GoogleMaps'], oCluster.bSingle ? strSingleImage : strMultiImage);
                var wid = oCluster.bSingle ? 15 : 22;
                var hgt = oCluster.bSingle ? 15 : 20;
                oIcon.iconSize = new GSize(wid, hgt);
                oIcon.iconAnchor = new GPoint(wid/2, hgt);
                oIcon.infoWindowAnchor = new GPoint(wid/2, 1);

                oCluster.oGMarker = new GMarker(oCluster.oPoint, oIcon);

                //  
                //  bind events

                GEvent.bind(oCluster.oGMarker, "mouseover", oCluster, fnHdlrMouseover);
                GEvent.bind(oCluster.oGMarker, "mouseout", oCluster, fnHdlrMouseout);
                GEvent.bind(oCluster.oGMarker, "click", oCluster, fnHdlrClick);

                return true;
            });
        };


    CMapMgr.prototype._showMarkers =
        function(nZoom, arrMarkers)
        {
            if (!this._map.isLoaded())
            {
                return;
            }
            if (!arrMarkers)
            {
                return;
            }

            if (typeof nZoom == 'undefined')
            {
                nZoom = this._map.getZoom();
            }
            var arrClusters = arrMarkers[nZoom];
            if (!arrClusters)
            {
                return;
            }

            var that = this;
            jQuery.each(arrClusters, function(i, oCluster) {
                if (!oCluster.oGMarker)
                {
                    return true;
                }

                that._map.addOverlay(oCluster.oGMarker);
                return true;
            });
        };


    CMapMgr.prototype._clearMarkers =
        function(nZoom, arrMarkers)
        {
            if (!this._map.isLoaded())
            {
                return;
            }
            if (!arrMarkers)
            {
                return;
            }

            if (typeof nZoom == 'undefined')
            {
                nZoom = this._map.getZoom();
            }
            var arrClusters = arrMarkers[nZoom];
            if (!arrClusters)
            {
                return;
            }

            var that = this;
            jQuery.each(arrClusters, function(i, oCluster) {
                if (!oCluster.oGMarker)
                {
                    return true;
                }

                that._map.removeOverlay(oCluster.oGMarker);
                return true;
            });
        };


    ////////////////////////////////////////////////////////////
    //
    //  _displaySimplePopup(szDivId, szText, oLatLng)
    //

    CMapMgr.prototype._displaySimplePopup =
        function(szDivId, szText, oLatLng)
        {
            //
            //  style and place the info popup
            //
            //  work out the pixel location in the browser window that the latlng represents
            //  a bit trickier than it should be - GMap should give it to us straight away
            //  via GMap.fromLatLngToDivPixel() but instead wee have to dig into the DOM with Firebug....grrrr....
            //
            //  basically, map.getContainer().offset[Left|Top] gives us the overall offset of the map from the 
            //  top-left corner of the screen, and '#map > div > div' (ie, the first div child of the first div child
            //  of the map div) has the internal map viewport offsets in style.[left|top]

            var $popup = jQuery(szDivId);
            $popup.text(szText);

            var pt = this._map.fromLatLngToDivPixel(oLatLng);
            var cntr = this._map.getContainer();
            if (pt && cntr)
            {
                pt.x += cntr.offsetLeft;
                pt.y += cntr.offsetTop;

                var hdiv = jQuery('#map > div > div')[0];
                if (hdiv && hdiv.style)
                {
                    if (hdiv.style.left)
                    {
                        pt.x += parseInt(hdiv.style.left);
                    }
                    if (hdiv.style.top)
                    {
                        pt.y += parseInt(hdiv.style.top);
                    }
                }

                var divpt = this._placeDiv(pt, $popup, 20);
                $popup.css({top: divpt.y+'px', left: divpt.x+'px'});
            }

            //$popup.corner("round 3px");
            $popup.fadeIn('fast');
        };


    ////////////////////////////////////////////////////////////
    //
    //  _placeDiv(pt, $div, xpad)
    //
    //  places the div on the screen making sure it is completely
    //  visible
    //

    CMapMgr.prototype._placeDiv =
        function(pt, $div, xpad)
        {
            var divpt = new GPoint;
            divpt.x = pt.x + xpad;
            divpt.y = pt.y - $div.height()/2;
            var $map = jQuery('#map');
            if (divpt.x + $div.width() > $map.width() - 20)
            {
                divpt.x = pt.x - $div.width() - xpad*2;
            }

            return divpt;
        };





};


//
//  init MapMgr global var

var gMapMgr = new CMapMgr();
