/**
 * Set up the map and interface.
 */
InitMap = {
    _progress: null, // Busy indicator
    
    init: function()
    {
        // Attempt to create map 
        window.map = createMap($('map'));
         
        // If failed, abort
        if (!map) {
            $('map').innerHTML = '<div class="error">Your browser appears to be incompatible with our interactive map. See the list of <a href="http://maps.google.com/support/bin/answer.py?hl=en&answer=16532">supported browsers</a> or take a look at the static <a href="/map/static.php">maps</a>.</div>';
            return false;
        }
        
        // Add map controls
        map.addControl(new GLargeMapControl3D());
        map.enableScrollWheelZoom();
        
        // Add the busy indicator
        var busyDiv = new Element('div', {id: 'busy'});
        $('map').appendChild(busyDiv);
        window.busy = new ProgressIndicator(busyDiv, 'active');

        // Enable the indicator (AJAX and stuff might take a while)
        window.busy.on('init');
        
        // Load the data
        var loader = new Loader('data.txt');
        loader.onFailure(this.dataFailed.bind(this));
        loader.onSuccess(this.dataLoaded.bind(this));

        // Hide the category controls until we have some points
        $('categories').setStyle({visibility: 'hidden'});
        loader.onSuccess(function() { $('categories').setStyle({visibility: 'visible'}); });
 
        // Set up the link to feature (note we must do this early on since we
        // need to replace a number of functions with wrapped functions to record
        // the necessary info)
        LinkTo.init(map, $('link-to'));
        
        // Set up the category togglers
        Categories.init($$('#categories li'));
 
        // Give the map to the Points manager
        Points.setMap(map);
 
        // Show the sidebar div
        $('sidebar').setStyle({display: 'block'});

        // Add the fullscreen toggle ability
        var fsToggle = addFullscreenToggle();
        
        // And the extended menu
        ExtendedMenu.init(fsToggle);

        // Set up the mark location feature
        MarkPoint.init(map, $('mark-point'));
        
        // Set up the filter feature
        Filter.init(map, $('filter'));
        
    },
    
    /**
     * Continue map setup when data is successfully loaded.
     */
    dataLoaded: function(response)
    {
        // Decode JSON
        var pointData = response.responseText.evalJSON();
        if (!pointData.status || pointData.status != 'ok') {
            this.dataFailed();
            return;
        }
        
        // Import the points to the category manager
        Categories.importPointData(pointData.points);
        
        // Enable the sidebar mouseovers
        addSidebarMouseovers($('sidebar'));
        
        // Set map to URL-given state
        LinkTo.updateMapFromQs();
        
        // Remove the busy indicator
        window.busy.off('init');
    },
    
    /**
     * Abort after failed AJAX
     */
    dataFailed: function()
    {
        $('sidebar').setStyle({display: 'none'});
        $('map').setStyle({display: 'none'});
        var div = new Element('div');
        div.addClassName('error');
        div.innerHTML = 'The map data failed to load. Please reload and try again. If the problem persists, <a href="http://www.gtaforums.com/index.php?showforum=2">report this fault to an administrator</a>.';
        $('map').insert({before:div});
    }
};

/**
 * Set up categories and add/remove points by category
 */
Categories = {
    _controls: null, // The control elements
    
    init: function(categories)
    {
        this._controls = new Hash();
        
        // Go through each control element
        for (var i = 0, l = categories.length; i < l; ++i) {

            var controlElement = categories[i];
            
            // Work out category ID
            var catId = controlElement.readAttribute('catId');
            if (!catId) {
                catId = 'unknown' + i;
            }
            
            // Create the toggler
            var toggle = new Toggler(controlElement, this.enable.bind(this, catId), this.disable.bind(this, catId));
            
            // Store control
            this._controls.set(catId, {element: controlElement, toggler: toggle});

            // Set starting state
            controlElement.writeAttribute('hovertext', 'Show '+controlElement.readAttribute('catTitle'));
        }
    },
    
    enable: function(catId)
    {
        // Check valid category
        if (!this._controls.get(catId)) {
            return false;
        }
        var category = this._controls.get(catId);
        
        // Check we have points
        if (!category.pointData) {
            return;
        }
        
        // Set the toggler to appropriate state (in case this enabling was not triggered
        // by the toggler)
        category.toggler.setState(true);
        
        // Mark as busy
        busy.on('adding '+catId);
        
        // Defer the rest of the function
        (function(){
            // Update control element
            var control = this._controls.get(catId).element;
            if (control) {
                control.writeAttribute('hovertext', 'Hide '+control.readAttribute('catTitle'));
                control.addClassName('active');
            }
            
            // Create the icon if necessary
            if (!category.icon) {            
                category.icon   = Points.createIcon(control.readAttribute('icon'),
                                                    [control.readAttribute('iconWidth'),control.readAttribute('iconHeight')]);
            }
            
            // Get the icon label
            var label  = control.readAttribute('iconLabel');
            var number = control.readAttribute('enumerable');
            
            // See if we have the markers yet and if not, create them
            if (!category.markers) {
                category.markers = new Array();
                for (var i=0, l=category.pointData.length; i<l; ++i) {
                
                    // Get the point data
                    var point           = category.pointData[i];
                    
                    // Create GMarkerOptions
                    var options         = {};
                    options.clickable   = point.text || point.vid || point.img ? true : false;
                    var autoPointLabel  = label + (number ? ' #'+point.n : '');
                    options.title       = point.label ? point.label : autoPointLabel;
                    
                    // Custom icon for this point?
                    if (point.icon) {
                        var icon = Points.createIcon('http://media.gtanet.com/gta4/images/map/icons/'+point.icon+'.png', [16,16]);
                    } else {
                        var icon = category.icon;
                    }
                    
                    // Create the marker
                    var marker          = Points.createMarker([point.lat,point.lng], icon, options);
                    
                    // Add the info window
                    if (options.clickable) {
                        Points.addInfoWindowToMarker(marker, this.createInfoWindowContent.bind(this, point, autoPointLabel), 'info_window');
                    }
                    
                    // Add useful info to the marker
                    marker.index      = point.n ? point.n : i;
                    marker.category   = catId;
                    marker.searchText = options.title.toLowerCase();
                    if (point.text) {
                        marker.searchText += ' ' + point.text.stripTags().toLowerCase();
                    }
                    
                    // Store the marker
                    category.markers[i] = marker;
                }
            }

            // Add markers to map
            for (var i=0, l=category.markers.length; i<l; ++i) {
                Points.addToMap(category.markers[i]);
            }
            
            // Clear busy
            busy.off('adding '+catId);
        }).bind(this).defer();
    },
    
    disable: function(catId)
    {
        // Check valid category
        if (!this._controls.get(catId)) {
            return false;
        }
        var category = this._controls.get(catId);

        // Check we have points
        if (!category.pointData) {
            return;
        }

        // Set the toggler to appropriate state (in case this disabling was not triggered
        // by the toggler)
        category.toggler.setState(false);
                
        // Mark as busy
        busy.on('removing '+catId);
        
        // Defer the rest of the function
        (function(){
            // Update control element
            var control = this._controls.get(catId).element;
            if (control) {
                control.writeAttribute('hovertext', 'Show '+control.readAttribute('catTitle'));
                control.removeClassName('active');
            }
            
            // Remove the points
            for (var i=0, l=category.markers.length; i<l; ++i) {
                Points.removeFromMap(category.markers[i]);
            }
            
            // Remove the info window if necessary
            if (Points.getOpenWindowMarker() && Points.getOpenWindowMarker().category == catId) {
                Points.closeWindow();
            }
            
            // Clear busy
            busy.off('removing '+catId);
            
        }).bind(this).defer();
    },
    
    importPointData: function(points)
    {
        for (var i in points) {
            if (this._controls.get(i)) {
                this._controls.get(i).pointData = points[i];
            }
        }
    },
    
    createInfoWindowContent: function(pointData, label)
    {
        function wrapContent(content) {
            return '<span class="info-title">'+label+'</span>'+"\n"+'<div class="info-content">'+content+'</div><br>';
        }
        tabs = new Array();
        if (pointData.text) {
            tabs.push({title: 'Info', content: pointData.text});
        }
        if (pointData.img) {
            tabs.push({title: 'Image', content: '<span id="loadtextimg">Loading...</span><center><a href="'+pointData.img+'" target="_blank"><img src="'+pointData.imgT+'" alt="Image" style="visibility:hidden" onload="if($(\'loadtextimg\')) { $(\'loadtextimg\').remove();this.style.visibility=\'visible\';map.getExtInfoWindow().resize();}"></a></center>'});
        }
        if (pointData.vid) {        
            var regex = /\/v\/([a-zA-Z0-9]{1,32})/;
            var vidT = '';
            var result = '';
            if (result = regex.exec(pointData.vid)) {
                vidT = result[1];
            }
            tabs.push({title: 'Video', content: '<img src="http://media.gtanet.com/gta4/images/map/interface/play.png" width="64" height="64" onclick="lightboxVideo(\''+label+'\', \''+pointData.vid+'\')" style="cursor:pointer;position:absolute;opacity:0.5;margin:46px 0 0 73px"><img width="210" height="156" src="http://i2.ytimg.com/vi/'+vidT+'/hqdefault.jpg" onerror="this.style.visibility=\'hidden\'">'});
        }
        if (tabs.length > 1) {
            var tabsheader = '<ul id="tabsheader">';
            var tabsbody   = '<div id="tabsbody">';
            
            tabsheader += '<li class="active">' + tabs[0].title + '</li>';
            tabsbody   += '<div class="tab">' + wrapContent(tabs[0].content) + '</div>';
            
            for (var i = 1, l=tabs.length; i<l; ++i) {
                tabsheader += '<li>' + tabs[i].title + '</li>';
                tabsbody   += '<div class="tab" style="display: none">' + wrapContent(tabs[i].content) + '</div>';
            }
            tabsheader += '</ul>';
            tabsbody   += '</div>';
            (function() { new Tabs($$('#tabsheader li'), $$('#tabsbody .tab')); }).defer();
            return tabsheader + tabsbody;
        } else {
            return wrapContent(tabs[0].content);
        }
    }
};

/**
 * Tabbed info windows
 */
Tabs = Class.create({
    _titles:   null,
    _contents: null,
    
    initialize: function(tabTitles, tabContents)
    {
        this._titles   = tabTitles;
        this._contents = tabContents;
        
        for (var i=0, l=tabTitles.length; i<l; ++i) {            
            tabContents[i].hide();
            tabTitles[i].observe('click', this.showTab.bind(this, i));
        }
        
        tabTitles[0].addClassName('active');
        tabContents[0].show();
    },
    
    showTab: function(showIndex)
    {
        for (var i=0, l=this._titles.length; i<l; ++i) {     
            if (i == showIndex) {
                this._contents[i].show();
                this._titles[i].addClassName('active');
            } else {
                this._contents[i].hide();
                this._titles[i].removeClassName('active');
            }
        }
        map.getExtInfoWindow().resize();
    }
});

/**
 * Lightbox a video
 */
function lightboxVideo(title, vidUrl)
{
    new Lightbox('<h2>'+title+'</h2><div class="content"><object width="480" height="295"><param name="movie" value="' + vidUrl + '&autoplay=1"></param><param name="allowFullScreen" value="true"></param><param name="allowscriptaccess" value="always"></param><embed src="' +vidUrl+ '&autoplay=1" type="application/x-shockwave-flash" allowscriptaccess="always" allowfullscreen="true" width="480" height="295"></embed></object></div>', 500);
}


/**
 * Add the fullscreen toggle ability 
 */
function addFullscreenToggle() 
{
    // Set up fullscreen capability
    var fs = new Fullscreen($('map-interface'));

    // Create the toggle element and position it in corner of map
    var control   = $(document.createElement('img'));
    control.src   = 'http://media.gtanet.com/gta4/images/map/interface/max.gif';
    control.title = 'View map in fullscreen mode';
    control.setStyle({position: 'absolute', top: 0, right: 0, zIndex: 1000});
    $('map').appendChild(control);
    
    // Make it toggleable
    var toggler = new Toggler(control, 
        function() {
            control.src   = 'http://media.gtanet.com/gta4/images/map/interface/min.gif';
            control.title = 'View map in normal mode';
            var center = map.getCenter();
            fs.goFullscreen();
            map.checkResize();
            map.setCenter(center);
        }, 
        function() {
            control.src   = 'http://media.gtanet.com/gta4/images/map/interface/max.gif';
            control.title = 'View map in fullscreen mode';
            var center = map.getCenter();
            fs.goNormal();
            map.checkResize();
            map.setCenter(center);
        });
    return toggler;
}

/**
 * Enable the sidebar mouseovers
 */
function addSidebarMouseovers(sidebar)
{    
    // Add the hover bars to all sidebar items
    for (var i = 0, icons = sidebar.select('li'), l=icons.length; i < l; ++i) {
        icons[i].observe('mouseover', showStatusBar);
        icons[i].observe('click', hideStatusBar);
        icons[i].observe('mouseout', hideStatusBar);
    }
}
function showStatusBar()
{
    // Do nothing if already exists or if we have no title
    if (this._statusDiv || !this.readAttribute('hovertext')) {
        return;
    }
    
    // Create the status div
    var div = new Element('div');
    div.innerHTML = this.readAttribute('hovertext');
    div.addClassName('hover-status-div');
    
    // Position it appropriately
    var liPos    = this.cumulativeOffset();
    var bodyDims = $(document.body).getDimensions();
    var liDims   = this.getDimensions();
    var diff     = Math.floor((liDims.height-24)/2);
    div.setStyle({top: liPos[1]+diff+'px', right: (bodyDims.width-liPos[0]+3)+'px'});
    
    // And add it to the document
    $(document.body).appendChild(div);
    this._statusDiv = div;
}

function hideStatusBar()
{
    if (this._statusDiv) {
        this._statusDiv.remove();
        this._statusDiv = null;
    }
}

/**
 * Extend a sidebar item rightwards
 */
ExtendedMenu = {
    _fsToggle: null,
    init: function(fsToggle)
    {
        this._fsToggle = fsToggle;
        
        // Watch for fullscreening so we can swap the styles
        this._fsToggle.enable = this._fsToggle.enable.wrap(function(callOriginal) {
            callOriginal();
            ExtendedMenu.useFullscreenStyles();
        });
        this._fsToggle.disable = this._fsToggle.disable.wrap(function(callOriginal) {
            callOriginal();
            ExtendedMenu.useNormalStyles();
        });
    },
    create: function(control, title, content)
    {
        // Create the infodiv
        var div = new Element('div');
        div.addClassName('infodiv-container');

        // Add the control (so we can reposition it based on control location)
        div.control = control;
        
        // See if we're fullscreened (needs to appear differently in fullscreen mode)
        if (this._fsToggle.isEnabled()) {
            this.useFullscreenStyles(div);
        } else {
            this.useNormalStyles(div);
        }
        
        // And add it to the document
        $(document.body).appendChild(div);
        
        // Fill info div with instructions
        div.innerHTML = '<div class="infodiv"><h3>'+title+'</h3><div class="infodiv-content">'+content+'</div></div>';
        
        // And return it
        return div;
    },
    useFullscreenStyles: function(div)
    {
        var divs = div ? [div] : $$('.infodiv-container');
        for (var i=0, l=divs.length; i<l; ++i) {
            var div = divs[i];
            var liPos    = div.control.cumulativeOffset();
            div.setStyle({position: 'absolute', top: liPos[1]+'px', right: document.viewport.getWidth()-liPos[0]+6+'px', left: 'auto',display: 'block'});
            div.addClassName('infodiv-container-fullscreen');
            $(document.body).appendChild(div);
        }
    },
    useNormalStyles: function(div)
    {
        var divs = div ? [div] : $$('.infodiv-container');
        for (var i=0, l=divs.length; i<l; ++i) {
            var div = divs[i];
            var liPos    = div.control.cumulativeOffset();
            div.setStyle({position: 'absolute', top: liPos[1]+'px', left: liPos[0]+div.control.getWidth()+'px', right: 'auto'});
            div.removeClassName('infodiv-container-fullscreen');
        }
    }
};

/**
 * Allow users to mark a point
 */
MarkPoint = {
    _map:       null,
    _control:   null,
    _markers:   null,
    _infoDiv:   null,
    icon:       null,
    
    init: function(map, control)
    {
        this._map     = map;
        this._control = control;
        this._markers = new Array();
        this._toggler = new Toggler(this._control, this.activate.bind(this), this.end.bind(this));
        this._infoDiv = null;
        this.icon     = Points.createIcon('http://media.gtanet.com/gta4/images/map/icons/mark-point.png', [16,16]);
    },
    activate: function()
    {
        // Listen for map clicks
        this._mapClickHandler = GEvent.addListener(map, 'click', this._mapClicked.bind(this));
        this._map.getDragObject().setDraggableCursor('pointer');
        this._mapRightClickListener = GEvent.addListener(this._map, 'singlerightclick', (function(p, s, overlay) {if(overlay && (overlay instanceof GMarker) && overlay.category && overlay.category == 'user') this.removePoint(overlay.index)}).bind(this));

        // Add desc
        this._infoDiv = ExtendedMenu.create(this._control, 'Point Marking Mode', 'Click a point on the map to add a marker.<br>Double click to add a description.<br>Right click to remove a marker.');
        
        // Update control
        this._control.writeAttribute('hovertext', 'Finish marking points');
        this._control.addClassName('hover');
        
        // Make markers draggable
        for (var i=0, l=this._markers.length; i<l; ++i) {
            this._markers[i].enableDragging();
        }
    },
    end: function()
    {
        // Stop listening for map clicks
        GEvent.removeListener(this._mapClickHandler);
        GEvent.removeListener(this._mapRightClickListener);
        this._map.getDragObject().setDraggableCursor('url(http://maps.google.com/intl/en_ALL/mapfiles/openhand.cur), default');

        // Remove info div
        this._infoDiv.remove();
    
        // Update control
        this._control.writeAttribute('hovertext', 'Mark a location');
        this._control.removeClassName('hover');
        
        // Make markers undraggable
        for (var i=0, l=this._markers.length; i<l; ++i) {
            this._markers[i].disableDragging();
        }
    },
    _mapClicked: function(overlay, position)
    {
        if (overlay) {
            return;
        }
        this.addPoint(position);
    },
    _markerDoubleClicked: function(marker)
    {
        // Do nothing if feature disabled
        if (!this._toggler.isEnabled()) {
            return;
        }
    
        // Get description
        if (marker.desc) {
            var newDesc = prompt('Enter a description for this point (optional):', marker.desc);
        } else {
            var newDesc = prompt('Enter a description for this point (optional):', '');
        }
        
        // Add the description
        this.addDescription(marker, newDesc);
    },
    addPoint: function(position)
    {
        var marker = Points.createMarker(position, this.icon, {title: 'User-marked point', draggable: true});
        Points.addToMap(marker);
        GEvent.addListener(marker, 'dblclick', this._markerDoubleClicked.bind(this, marker));
        
        marker.index = this._markers.last() ? this._markers.last().index+1 : 1;
        marker.category = 'user';
        marker.desc     = '';
        this._markers.push(marker);
        return marker;
    },
    addDescription: function(marker, desc)
    {
        // Make input safe
        if (desc) {
            desc = desc.stripTags().escapeHTML();
        }
        // Remove info window if no description
        else {
            Points.removeInfoWindowFromMarker(marker);
            return;
        }
        
        // Recreate marker with description in tooltip
        var position  = marker.getLatLng();
        var index     = marker.index;
        this._markers = this._markers.without(marker);
        Points.removeFromMap(marker);

        var marker      = Points.createMarker(position, this.icon, {title: 'User-marked point ('+desc+')', draggable: true});
        marker.index    = index;
        marker.category = 'user';
        marker.desc     = desc;
        this._markers.push(marker);
        Points.addToMap(marker);
        
        // And add the info window
        Points.addInfoWindowToMarker(marker, (function(desc,i){return '<span class="info-title">User-marked point</span><div class="info-content">'+desc+'</div><br>';}).curry(desc, marker.index), 'info_window');
    
    },
    removePoint: function(index)
    {
        var marker = null;
        if (marker = this._markers.detect(function(n){return n.index==index})) {
            Points.removeFromMap(marker);
            this._markers = this._markers.without(marker);
        }
    }
};

/**
 * Allow users to filter the current selection of points.
 */
Filter = {
    _map:       null,
    _control:   null,
    _toggler:   null,

    init: function(map, control)
    {
        this._map       = map;
        this._control   = control;
        this._toggler   = new Toggler(this._control, this.activate.bind(this), this.end.bind(this));
        
        // Re-apply filter on category change
        var catControls = $$('#categories li');
        var observer    = this.reapplyOnCatChange.bind(this);
        for (var i=0, l=catControls.length; i<l; ++i) {
            catControls[i].observe('click', observer);
        }
    },
    activate: function()
    {
        // Update toggler (in case activation is from somewhere else)
        this._toggler.setState(true);
    
        // Update status bar
        this._infoDiv = ExtendedMenu.create(this._control, 'Filter Displayed Points', 'Enter a search term to filter the displayed points by: <input type="text" id="filter-text"><br><span id="filter-shown">0</span> matches, <span id="filter-hidden">0</span> hidden');

        // Listen for keyup and apply filter
        $('filter-text').observe('keyup', this.applyFromField.bind(this));
        
        // Focus
        $('filter-text').focus();
        
        // Update count
        $('filter-shown').innerHTML = Points.getMarkersOnMap().length;
    
        // Update control
        this._control.writeAttribute('hovertext', 'Remove Filter');
        this._control.addClassName('hover');
    },
    end: function() 
    {
        // Stop listening
        $('filter-text').stopObserving('keyup');
    
        // Update status bar
        this._infoDiv.remove();

        // Show all
        var markers  = Points.getMarkersOnMap();
        for (var i=0,l=markers.length; i<l; ++i) {
            markers[i].show();
        }
        
        // Update control
        this._control.writeAttribute('hovertext', 'Filter Selection');
        this._control.removeClassName('hover');
    },
    apply: function(filterBy)
    {
        // Do nothing if disabled
        if (!this._toggler.isEnabled()) {
            return false;
        }
    
        var filterBy = filterBy.toLowerCase();
        var markers  = Points.getMarkersOnMap();
        
        var matched = 0;
        var hidden  = 0;
        
        for (var i=0,l=markers.length; i<l; ++i) {
            var currentMarker = markers[i];

            // Ignore non-searchable points
            if (!currentMarker.searchText) {
                continue;
            }
            
            if (!filterBy || currentMarker.searchText.indexOf(filterBy) >= 0) {
                ++matched;
                currentMarker.show();
            } else {
                ++hidden;
                currentMarker.hide();
            }
        }
        
        // Check for open info window - do we need to close it?
        var open = null;
        if (open = Points.getOpenWindowMarker()) {
            if (open.searchText.indexOf(filterBy) == -1) {
                Points.closeWindow();
            }
        }

        $('filter-shown').innerHTML = matched;
        $('filter-hidden').innerHTML = hidden;
    },
    applyFromField: function()
    {
        this.apply(this.getFieldValue());
    },
    reapplyOnCatChange: function()
    {
        // Do nothing if disabled
        if (!this._toggler.isEnabled()) {
            return false;
        }
        (function() {
        this.applyFromField.bind(this).defer();
        }).bind(this).defer();
    },
    getFieldValue: function()
    {
        return $('filter-text') ? $('filter-text').value.toLowerCase() : '';
    },
    setFieldValue: function(value)
    {
        if ($('filter-text')) {
            $('filter-text').value = value;
        }
    }
};

/**
 * Link to current view feature.
 */
LinkTo = {
    _map:       null,
    _control:   null,
    _resultDiv: null,
    
    // Map data
    activeCats: new Array(),
    openCat:    '',
    openId:     0,

    init: function(map, control)
    {
        this._map       = map;
        this._control   = control;
        this._control.observe('click', this.controlClicked.bind(this));
        
        // Add function wrappers to keep us updated of map state
        Categories.enable = Categories.enable.wrap(function(callOriginal, catId) {
            LinkTo.activeCats.push(catId);
            callOriginal(catId);
        });
        Categories.disable = Categories.disable.wrap(function(callOriginal, catId) {
            LinkTo.activeCats = LinkTo.activeCats.without(catId);
            callOriginal(catId);
        });
    },
    controlClicked: function()
    {
        // Already open? Close
        if (this._resultDiv) {
            this.close();
            return;
        }
    
        // Create the infodiv
        this._resultDiv = new Element('div', {id: 'link-to-result'});

        // Position it appropriately
        this._resultDiv.setStyle({position: 'absolute', bottom: '20px', left: '20px', width: this._map.getContainer().getWidth()-50+'px'});

        // Create div content
        this._resultDiv.innerHTML = '<input type="text" id="link-to-text">';
        
        // And add it to the document
        this._map.getContainer().appendChild(this._resultDiv);
        
        // And update the text field
        var field = $('link-to-text')
        field.setStyle({width: '98%'});
        field.value = this.getUrl();
        field.focus();
        field.select();

        // Observe blurring and close when field no longer focused OR when map is clicked
        field.observe('blur', this.close.bind(this))
        this._mapListener = GEvent.addListener(this._map, 'click', this.close.bind(this));
        
        // Show feature as active
        this._control.addClassName('hover');
    },
    close: function()
    {
        this._resultDiv.remove();
        this._resultDiv = null;
        this._control.removeClassName('hover');
        GEvent.removeListener(this._mapListener);
    },
    getUrl: function()
    {
        // Get required data to remember state
        var userMarkers = MarkPoint._markers;
        var lats        = userMarkers.invoke('getLatLng').invoke('lat');
        var lngs        = userMarkers.invoke('getLatLng').invoke('lng');
        var txts        = userMarkers.pluck('desc');
        var userPoints  = new Array();
        for (var i=0, l=lats.length; i<l; ++i)
        {
            userPoints.push([lats[i], lngs[i], txts[i]].join('|'));
        }
        var position   = this._map.getCenter();
        var open       = Points.getOpenWindowMarker();
        
        var allParams = {
            lat: position.lat(),
            lng: position.lng(),
            z:   this._map.getZoom(),
            c:   this.activeCats.uniq(),
            u:   userPoints,
            mc:  open && open.category ? open.category : null,
            mid: open && open.index    ? open.index    : null,
            f:   Filter.getFieldValue()
        };
        
        // Cut down to params for which we have a value only
        var givenParams = {};
        for (var i in allParams) {
            if (allParams[i]) {
                givenParams[i] = allParams[i];
            }            
        }
        
        return location.protocol + '//' + location.host + location.pathname + '?' + Object.toQueryString(givenParams);
    },
    updateMapFromQs: function()
    {
        var params = location.search.toQueryParams();
        
        if (params.lat && params.lng) {
            this._map.setCenter(new GLatLng(params.lat, params.lng));
        }
        if (params.z) {
            this._map.setZoom(parseInt(params.z));
        }
        if (params.c) {
            if (params.c instanceof Array) {
                params.c.each(Categories.enable.bind(Categories));
            } else {
                Categories.enable(params.c);
            }
        }
        if (params.ulat && params.ulng) { // old-style user points
            var userPoint = MarkPoint.addPoint(new GLatLng(params.ulat, params.ulng));
            userPoint.disableDragging();
            if (params.ut) {
                MarkPoint.addDescription(userPoint, params.ut);
            }
        }
        if (params.u) {
            if (!(params.u instanceof Array)) {
                params.u = [params.u];
            }
            for (var i=0,l=params.u.length; i<l; ++i) {
                var bits = params.u[i].split('|');
                if (bits.length != 3) {
                    continue;
                }
                var userPoint = MarkPoint.addPoint(new GLatLng(bits[0], bits[1]));
                userPoint.disableDragging();
                if (bits[2]) {
                    MarkPoint.addDescription(userPoint, bits[2]);
                }
            }
        }
        (function() { // Wait for cats + markers to get sorted out
            if (params.mc && params.mid) {
                var markers = Points.getMarkersOnMap();
                var toOpen  = markers.find(function(m){return m.index == params.mid && m.category == params.mc});
                if (toOpen) {
                    GEvent.trigger(toOpen, 'click');
                }
            }
            if (params.f) {
                Filter.activate();
                Filter.setFieldValue(params.f);
                Filter.apply(params.f);
            }
        }).defer();
    }
};


/**
 * Wait for domready, then load the map
 */
document.observe('dom:loaded', InitMap.init.bind(InitMap));