/**
 * Class-like function to manage Google Map for cases
 *
 * @author Peter Kruithof
 */
function FilterMap(nodeId, urlPrefix)
{
    // make sure jquery is defined
    if ($ == undefined) throw new Error(';FilterMap : JQuery must be included properly in order for the filtermap to work.');

    // set properties
    this.gMap;
    this.containerNodeId = nodeId;
    this.mapNodeId = 'gmap';
    this.urlPrefix = urlPrefix;
    this.urlExtension = '.json';
    this.caseTypes = [];
    this.markers = {};
    this.bounds;
    this.dataProvider = new CaseDataProvider();
    this.filters = {};
    this.filterform;
    this.hashFilters; // filters submitted by using a hash in the url, for deep linking

    /**
     * Event handler for movement of the map.
     * Filters are applied for the current bounds, when moved.
     */
    this.onChangePosition = function()
    {
        this.bounds = this.gMap.getBounds();
        this.setFilter('fldgeo_southwest', this.bounds.getSouthWest().toUrlValue());
        this.setFilter('fldgeo_northeast', this.bounds.getNorthEast().toUrlValue());

        this.applyFilters();
    }

    this.onChangeState = function()
    {
        var icon = this.getLoadingIcon();

        if (this.dataProvider.state == this.dataProvider.LOADING) {
            $(icon).removeClass('nodisplay');
        } else {
            $(icon).addClass('nodisplay');
        }
    }

    /**
     * @return The object containing the GMap and filter form
     */
    this.getContainer = function()
    {
        return $("#" + this.containerNodeId);
    }

    /**
     * @return The object containing the GMap
     */
    this.getMap = function()
    {
        return $("#" + this.mapNodeId);
    }

    /**
     * @param {GLatLng} coords The coords to create a marker for
     */
    this.getMarker = function(coords)
    {
        if (!coords || (!(coords instanceof GLatLng))) throw new Error(';FilterMap.getMarker : invalid coords given: needs to be instance of GLatLng');

        var id = latLngToString(coords);

        // create marker for these coords, if necessary
        if (!(id in this.markers)) {

            // get icon based on current selected status
            var currentStatus = this.getFilterFormStatusValue();
            var icon = (currentStatus != '') ? CasesIcon.get(currentStatus) : null;

            this.markers[id] = new CasesMarker(coords, icon);
            this.gMap.addOverlay(this.markers[id].marker);
        }

        return this.markers[id];
    }

    this.getLoadingIcon = function()
    {
        if (this.loadingIcon == undefined) {
            this.loadingIcon = document.createElement('img');
            this.loadingIcon.setAttribute('src', '/images/i_loading.gif');
            this.loadingIcon.setAttribute('id', 'loading-icon');
            $(this.loadingIcon).addClass('nodisplay');
            this.getContainer().append(this.loadingIcon);
        }
        return this.loadingIcon;
    }

    this.setDefaultCaseTypes = function(caseTypes)
    {
        this.caseTypes = caseTypes;
    }

    /**
     * Creates a form with filters and attaches it to the map container
     * Also, rules are applied to the form.
     */
    this.loadFilterForm = function(result)
    {
        // get the last fetched result
        if (result && !result.isError) {

            // start the html
            var form = '';

            // create filters
            var filters = '';
            var rules = {};

            this.filterform = {};

            if (result.page_data.filters) {

                var filter;
                for (var a in result.page_data.filters) {
                    filter = result.page_data.filters[a];

                    if (filter['type']) {
                        if (filter['type'] == 'text') {

                            var label = filter['label'];
                            var id = filter['attributes']['id'];
                            var name = a;
                            var accesskey = filter['attributes']['accesskey'] || '';
                            var tabindex = filter['attributes']['tabindex'] || '';
                            var value = (this.hashFilters && (name in this.hashFilters)) ? this.hashFilters[name] : '';

                            filters += '<label for="' + id + '">' + label + '</label><input value="' + value + '" type="text" id="' + id + '" name="' + name + '" accesskey="' + accesskey + '" tabindex="' + tabindex + '"/>';

                        } else if (filter['type'] == 'group') {

                            // group can be of type 'radio', 'checkbox', or 'select'
                            // all groups contain options
                            if ((filter['options']) && (typeof(filter['options']) == 'object')) {

                                var default_options = '';
                                var other_options = '';

                                for (var b in filter['options']) {

                                    var checked = '';
                                    var optionLabel = filter['options'][b]['label'];
                                    var type = filter['options'][b]['attributes']['type'];
                                    var id = filter['options'][b]['attributes']['id'];
                                    var tabindex = filter['options'][b]['attributes']['tabindex'] || '';

                                    if (type == 'checkbox') {
                                        var name = a + '[' + filter['options'][b]['attributes']['name'] + ']';
                                        var value = filter['options'][b]['attributes']['value'] || '';
                                    } else if (type == 'radio') {
                                        var name = a;
                                        var value = filter['options'][b]['attributes']['value'];
                                    }

                                    // set default case types
                                    if ((a == 'fldcasetypes') && !this.hashFilters && in_array(filter['options'][b]['attributes']['name'], this.caseTypes)) {
                                        this.setFilter(name, '1');
                                    }

                                    // set default status
                                    if ((a == 'fldstatus') && !this.hashFilters && (value == 'pending')) {
                                        this.setFilter(name, value);
                                    }

                                    // compose options html
                                    if ((a == 'fldcasetypes') && in_array(filter['options'][b]['attributes']['name'], this.caseTypes)) {
                                        default_options += '<div class="option"><input class="' + type + '" type="' + type + '" id="' + id + '" name="' + name + '" value="' + value + '" tabindex="' + tabindex + '"/><label for="' + id + '">' + optionLabel + '</label></div>';
                                    } else if (a == 'fldstatus') {
                                        other_options += '<div class="option"><input class="' + type + '" type="' + type + '" id="' + id + '" name="' + name + '" value="' + value + '" tabindex="' + tabindex + '"/><label for="' + id + '" class="status ' + value + '">' + optionLabel + '</label></div>';
                                    } else {
                                        other_options += '<div class="option"><input class="' + type + '" type="' + type + '" id="' + id + '" name="' + name + '" value="' + value + '" tabindex="' + tabindex + '"/><label for="' + id + '">' + optionLabel + '</label></div>';
                                    }


                                    // overwrite filters with values from hash
                                    if (this.hashFilters && (name in this.hashFilters)) {
                                        this.setFilter(name, this.hashFilters[name]);
                                    }
                                }

                                var select = '';
                                if (type == 'checkbox') {
                                    select = '<span>' + getText('Select') + ' ' + '<span class="select-all">' + getText('SelectAll') + '</span>/<span class="select-none">' + getText('SelectNone') + '</span></span>';
                                }
                                var groupclass = 'group' + ((a == 'fldstatus') ? ' status' : '');
                                filters += '<div class="' + groupclass + '">' + select + default_options + '<hr/>' + other_options + '</div>';
                            }
                        }

                        // add rules
                        if (filter['rules']) {
                            rules[a] = [];
                            for (var b in filter['rules']) {
                                rules[a].push(filter['rules'][b]);
                            }
                        }
                    }
                }

                this.filterform.rules = rules;

                // add submit button
                if (filters !== '') {
                    filters = '<div id="filters">' + filters + '</div><p><input type="submit" class="button submit" value="' + getText('DCPFilterSubmitButton') + '"/></p>';
                }
            }

            // create form
            form = '<form action="'+ this.urlPrefix + '/" method="get"><fieldset><legend>' + getText('DCPCasesFiltersDescription') + '</legend>' + filters + '</fieldset></form>';

            // append to container
            this.getContainer().append('<div id="filterform">' + form + '</div>');

            // set the height of the form
            var height = this.getMap().height() - 100;
            $('#filterform').height(height);

            // the height of the filters is set by subtracting the legend and <p> tag's height from the filter's parent height
            var filters = $('#filters');
            var parent = filters.parent();
            height = height - parent.find('legend').height() - parent.find('p').height() - 5; // minus 5 pixels, due to borders or whatever
            filters.height(height);

            // add events for selecting checkboxes
            $('#filterform .select-all').bind('click', function() { $(this).parent().parent().find(':checkbox').each(function() { this.checked = true; }); });
            $('#filterform .select-none').bind('click', function() { $(this).parent().parent().find(':checkbox').each(function() { this.checked = false; }); });

            // add event to form and trigger it for the first time
            var self = this;
            applyEvent($('#filterform form').get(0), 'submit', function(e) { self.onSubmitFilterForm(e); });


            // set all filters
            for (var a in this.filters) {
                var input = $("#filterform form :input[name='" + a + "'][value='" + this.filters[a] + "']").attr('checked', 'checked');
            }

            this.onSubmitFilterForm();
        }
    }

    /**
     * Returns the value of the checked radio input for status filter
     */
    this.getFilterFormStatusValue = function()
    {
        var status = $("#filterform :radio[name$='status']:checked");
        if (status.size() > 0) {
            return status.get(0).value;
        } else {
            return '';
        }
    }

    /**
     * Event handler invoked when filter form is submitted.
     * Sets/unsets filters and applies them.
     *
     * @param {Event} e The event
     */
    this.onSubmitFilterForm = function(e)
    {
        // never submit the form
        if (e) cancelEvent(e);

        // iterate through all formfields
        var formfields = $('#filterform form').get(0).elements;

        // only search if at least one case type is checked
        if (!($('#filterform form :checkbox:checked').size() > 0)) {
            alert(getText('DCPNoCaseTypesChecked'));
            return;
        }

        for (var i = 0; i < formfields.length; i++) {

            // skip nameless formfields
            if (formfields[i].name) {

                // only specify value for checkboxes when checked
                var value = formfields[i].value;
                if (formfields[i].type == 'checkbox') value = (formfields[i].checked) ? 1 : 0;
                if (formfields[i].type == 'radio') value = (formfields[i].checked) ? value : 0;

                // check for rules
                if (this.filterform.rules[formfields[i].name]) {
                    var rules = this.filterform.rules[formfields[i].name];
                    for (var j = 0; j < rules.length; j++) {

                        // if at least one of the fields doesn't validate, stop the entire process
                        if (!this.validateFormField(value, rules[j]['format'])) {
                            alert(rules[j]['message']);
                        }
                    }
                }

                if (value) {
                    // filter is set, apply it
                    this.setFilter(formfields[i].name, value);

                } else {
                    // filter not set, remove it, but not for radio buttons!
                    if (formfields[i].type != 'radio') {
                        this.removeFilter(formfields[i].name);
                    }
                }
            }
        }

        // finally, apply the filters
        this.applyFilters();
    }

    /**
     * Validates a form field by testing it against an regular expression
     *
     * @param    {String}    value     The value to check
     * @param    {String}    format     The pattern to test against, eg: <code>/[a-z]/i</code>. Will be converted to an expression.
     * @return   {boolean}   The validation outcome
     */
    this.validateFormField = function(value, format)
    {
        // split the modifier from the expression, and create a new one
        var res = (/\/(.+)\/([i|g]+)*/).exec(format);
        if ((res instanceof Array) && (res.length > 1)) {
            var pattern = res[1] || '//';
            var modifier = res[2] || '';
            var regex = new RegExp(pattern, modifier);

            // only test when value is not empty
            if ((value !== '') && !regex.test(value)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Sets a filter. Be sure to call {@link applyFilters} for changes to take effect.
     */
    this.setFilter = function(name, value)
    {
        this.filters[name] = value;
    }

    /**
     * Removes a filter. Be sure to call {@link applyFilters} for changes to take effect.
     */
    this.removeFilter = function(name)
    {
        delete this.filters[name];
    }

    /**
     * Applies a filter to the map (ie: cases with that criteria are searched).
     *
     * @return void
     */
    this.applyFilters = function()
    {
        var url = this.urlPrefix + this.urlExtension + '?';

        // these parameters are required and has to be set default values
        this.filters['show'] = 'list';
        this.filters['sc'] = 'datetime_content';
        this.filters['so'] = 'desc';

        // create the url
        var urlparams = [];
        for (var a in this.filters) urlparams.push(a + '=' + this.filters[a]);

        updateDocumentLocation(this.filters);

        // fetch the results
        this.dataProvider.fetch(url + urlparams.join('&'));
    }

    this.applyView = function(view)
    {
        // subtract coords
        if ('coords' in view) {
            var coords = view['coords'];
            delete view['coords'];
        }

        for (var a in view) {
            this.setFilter(a, view.a);
        }

        this.applyFilters();
    }

    /**
     * Initializes properties and loads the map.
     */
    this.load = function(x, n, e, s, w)
    {
        if (this.containerNodeId == undefined) throw new Error(';FilterMap.load : A node id must be specified in order for the map to work.');

        // create node for the gmap
        this.getContainer().append('<div id="gmap"></div>');

        // listen to updates from dataprovider
        this.dataProvider.addEventListener('update', this.update, this);
        this.dataProvider.addEventListener('state', this.onChangeState, this);

        // max the map's size
        this.sizeToMaximum();

        // also do this when window is resized
        window.onresize = Delegate.create(this, this.sizeToMaximum);

        if (GBrowserIsCompatible()) {

            // get hash
            var hash = document.location.hash;

            // if we have a hash, use that as filters, otherwise, use default form settings
            if (hash != '') {
                hash = hash.substring(1); // strip off hash sign
                this.hashFilters = {};
                var queryparams = (hash.indexOf('&') > 0) ? hash.split('&') : [hash];
                for (var i = 0; i < queryparams.length; i++) {
                    var param = queryparams[i].split('=');
                    this.hashFilters[param[0]] = param[1];
                }
            }

            if (this.hashFilters) {

                // overwrite bounds if available
                if (('fldgeo_northeast' in this.hashFilters) && ('fldgeo_southwest' in this.hashFilters)) {
                    var ne = this.hashFilters['fldgeo_northeast'].split(',');
                    var sw = this.hashFilters['fldgeo_southwest'].split(',');
                    n = ne[0];
                    e = ne[1];
                    s = sw[0];
                    w = sw[1];
                }

                // set coords for opened marker
                if ('coords' in this.hashFilters) {
                    this.setFilter('coords', this.hashFilters['coords']);
                }
            }

            // create a new map
            this.gMap = new GMap2(this.getMap().get(0), {'mapTypes' : G_DEFAULT_MAP_TYPES});
            this.gMap.addControl(new GMapTypeControl());
            this.gMap.addControl(new GLargeMapControl());
            this.gMap.addControl(new GScaleControl());

            // set the bounds of the map
            this.bounds = new GLatLngBounds(new GLatLng(s, w), new GLatLng(n, e));

            // set bounds, zoom and center
            var zoomlevel = this.gMap.getBoundsZoomLevel(this.bounds);
            var center = this.bounds.getCenter();

            // set center point (required to make the map work)
            this.gMap.setCenter(center, zoomlevel);

            // set the initial bounds as filters
            this.bounds = this.gMap.getBounds();
            this.setFilter('fldgeo_southwest', this.bounds.getSouthWest().toUrlValue());
            this.setFilter('fldgeo_northeast', this.bounds.getNorthEast().toUrlValue());

            // listen to map dragging
            var callback = Delegate.create(this, this.onChangePosition);
            GEvent.addListener(this.gMap, "dragend", callback);
            GEvent.addListener(this.gMap, "zoomend", callback);

            // load the modal
            Modal.load();

            // load the form
            var self = this;
            $.getJSON(this.urlPrefix + this.urlExtension, function(result) { self.loadIcons(result); self.loadFilterForm(result); });
        }
    }

    this.loadIcons = function(result)
    {
        if (result && !result.isError) {
            if (result.page_data.icons) {
                for (var a in result.page_data.icons) {
                    CasesIcon.add(a, result.page_data.icons[a]);
                }
            }
        }
    }


    /**
     * Resizes the map to the maximum boundaries of the content node, calculated by screen size.
     */
    this.sizeToMaximum = function()
    {
        var map = this.getMap();
        var content = $("#content");

        if (map && content) {

            // calculate the maximum width
            var maxWidth = content.width() - Number(content.css('padding-left').replace(/px/, '')) - Number(content.css('padding-right').replace(/px/, ''));
            var maxHeight = document.documentElement.clientHeight - map.offset().top;

            map.width(maxWidth);
            map.height(maxHeight);

            // also set the width/height of infowindows
            FilterMap.infoWindowWidth = .4*maxWidth;

        } else {
            throw new Error('Could not find appropriate nodes.');
        }
    }

    /**
     * Updates the entire map by checking the points stored in the DataProvider.
     * All points that are located within viewport boundaries are updated this way.
     * All markers that are not located within viewport boundaries, or that do not match the set criteria, are removed.
     */
    this.update = function()
    {
        var marker, caseData
        var openedMarker = ('coords' in this.filters) ? this.filters['coords'] : '';

        // remove all markers
        for (var a in this.markers) {
            this.gMap.removeOverlay(this.markers[a].marker);
            delete this.markers[a];
        }

        // if items per page exceeds the acceptable limit, display a notice
        var result = this.dataProvider.lastResult;

        if (result.page_data.resultcount > result.page_data.itemsperpage) {

            // create the url
            var url = this.urlPrefix + '/?';
            var urlparams = [];
            for (var a in this.filters) urlparams.push(a + '=' + this.filters[a]);
            url += urlparams.join('&');

            var text = sprintf(getText('DCPTooManyResults'), result.page_data.itemsperpage, '<a href="' + url + '">', '</a>');

            Modal.display(text, 'notice');

        } else if (result.page_data.resultcount == 0) {

            Modal.display(getText('DCPNoResults'), 'notice');

        }

        // loop through dataprovider's points
        var coordinates = {};
        for (var a in this.dataProvider.points) {
            if (this.bounds.containsLatLng(this.dataProvider.points[a].coords)) {

                // get the marker (a new marker will be created if necessary)
                marker = this.getMarker(this.dataProvider.points[a].coords);

                // remove previously set cases: we want to rebuild the whole thing
                marker.removeCases();

                for (var b in this.dataProvider.points[a].children) {
                    caseData = this.dataProvider.points[a].children[b];

                    // add case (if not already added)
                    marker.addCase(caseData.container_id,
                                   caseData.title,
                                   {'name' : caseData.subtype_name, 'title' : caseData.subtype_title},
                                   new Date(caseData.datetime_content),
                                   caseData.status,
                                   caseData.description,
                                   this.urlPrefix + '/' + caseData.container_id + '/',
                                   caseData.publicarea);
                }

                // see if marker contents changed
                var oldContents = marker.getContents();
                var newContents = marker.rebuildContents();
                if (oldContents !== newContents) {

                    // bind the contents to the infowindow
                    marker.bindContents();

                    // see if this is the opened marker (by checking marker id's)
                    if (latLngToString(this.dataProvider.points[a].coords) == openedMarker) {

                        // marker was opened, open it again
                        marker.marker.openInfoWindow(newContents, {'maxWidth' : FilterMap.infoWindowWidth, 'maxHeight' : FilterMap.infoWindowHeight});
                    }
                }
            }
        }
    }
}

// class properties
FilterMap.infoWindowWidth = 240;
FilterMap.infoWindowHeight = 400;

/**
 * Helper class to abstract the case data from the map view.
 * Stores all different points, and groups the cases that have that exact coordinates.
 */
function CaseDataProvider()
{
    this.LOADING = 'loading';
    this.IDLE = 'idle';
    this.ERROR = 'err';

    this.lastResult;
    this.state;
    this.loadingIcon;
    this.listeners = {};
    this.points = {};
    this.timeoutID;

    /**
     * Returns a point for the given latitude and longitude.
     * If a point doesn't exist, it is created.
     *
     * @param {Number} lat The latitude
     * @param {Number} lng The longitude
     */
    this.getPoint = function(lat, lng)
    {
        var latlng = new GLatLng(lat, lng);
        var id = latlng.toUrlValue();

        if (!this.points[id]) {
            this.points[id] = {'coords': latlng, 'children' : {}};
        }

        return this.points[id];
    }

    this.setState = function(state)
    {
        this.state = state;
        this.dispatchEvent('state');
    }

    this.setTimeout = function(miliseconds)
    {
        this.clearTimeout();

        var self = this;
        this.timeoutID = setTimeout(function() { self.timeout(); }, miliseconds);
    }

    this.clearTimeout = function()
    {
        if (this.timeoutID) {
            clearTimeout(this.timeoutID);
        }
    }

    this.timeout = function()
    {
        this.setState(this.ERROR);
        Modal.display(getText('DCPTimeoutError'), 'error');
    }

    /**
     * Registers an event listener for this class.
     *
     * @param {String}      event   The event to listen to.
     * @param {Function}    func    The function to call when event is broadcasted.
     * @param {Object}      scope   The scope to preserve while applying the callback.
     */
    this.addEventListener = function(event, func, scope)
    {
        if (!this.listeners[event]) this.listeners[event] = [];

        for (var i = 0; i < this.listeners[event].length; i++) {
            if (this.listeners[event][i].callback === func) return;
        }
        this.listeners[event].push({'callback' : func, 'scope' : scope});
    }

    /**
     * Dispatches an event to all registered listeners.
     *
     * @param {String} event The event to dispatch
     */
    this.dispatchEvent = function(event)
    {
        if (this.listeners[event] && (this.listeners[event].length > 0)) {
            for (var i = 0; i < this.listeners[event].length; i++) {
                this.listeners[event][i].callback.call(this.listeners['update'][i].scope);
            }
        }
    }

    /**
     * Executes an XmlHttpRequest, and delegates the returned JSON result to {@link parseResult}
     */
    this.fetch = function(uri)
    {
        if (!uri) throw new Error(';CaseDataProvider.fetch : argument \'uri\' missing or invalid');

        Modal.close();

        // set timer for possible timeout
        this.setTimeout(30000); // 30 seconds should be more than enough

        this.setState(this.LOADING);
        $.getJSON(uri, Delegate.create(this, this.parseResult));

    }

    /**
     * Event handler called from {@link fetch}.
     * Adds all new points and children and dispatches an update event.
     *
     * @param {Object} result The result object returned by the JSON request
     */
    this.parseResult = function(result)
    {
        this.clearTimeout();

        this.setState(this.IDLE);

        // everything ok?
        if (result && !result.isError) {

            // store the result
            this.lastResult = result;

            if (result.page_data) {

                // clear points
                this.points = {};

                if (result.page_data.children) {

                    var child, point;
                    for (var a in result.page_data.children) {
                        child = result.page_data.children[a];

                        if (child.geo_location_lat && child.geo_location_lng) {
                            point = this.getPoint(child.geo_location_lat, child.geo_location_lng);
                            point.children[child.container_id] = child;
                        }
                    }
                }
            }

            // let listeners know we've been updated
            this.dispatchEvent('update');

        } else {

            // TODO something went wrong, display something on screen?
            throw new Error('Invalid result returned.');
        }
    }
}

var CasesIcon =
{
    'icons' : {},

    'add' : function(type, opts)
    {
        if (!this.icons[type]) {
            var image;
            if (opts.image) image = opts.image;
            this.icons[type] = new GIcon(G_DEFAULT_ICON, image);

            for (var a in opts) {
                var value = opts[a];

                if (value != null) {
                    if (a == 'imageMap') {
                        // fix type if needed
                        if (!(value instanceof Array)) {
                            var values = value.toString().replace(/\\s/, '').split(',');
                            value = [];
                            for (var i = 0; i < values.length; i++) {
                                value.push(parseInt(values[i]));
                            }
                        }
                    } else if ((a == 'iconSize') || (a == 'shadowSize')) {
                        // fix type if needed
                        if ((typeof value) == 'string') {
                            var values = value.replace(/\\s/, '').split(',');
                            value = new GSize(values[0], values[1]);
                        }
                    } else if ((a == 'iconAnchor') || (a == 'infoWindowAnchor')) {
                        // fix type if needed
                        if ((typeof value) == 'string') {
                            var values = value.replace(/\\s/, '').split(',');
                            value = new GPoint(values[0], values[1]);
                        }
                    }
                    this.icons[type][a] = value;
                }
            }
        }
    },

    'get' : function(type)
    {
        return this.icons[type];
    }
}

/**
 * Represents a marker on the map, that can contain multiple cases.
 * New cases can be added at any time, while maintaining the marker's state.
 *
 * @param {GLatLng} The coordinates of the marker.
 */
function CasesMarker(coords, icon)
{
    if (!coords || !(coords instanceof GLatLng)) throw new Error(';CasesMarker : first argument must be of type GLatLng');
    if (!icon || !(icon instanceof GIcon)) throw new Error(';CasesMarker : second argument must be of type GIcon');

    this.coords = coords;
    this.cases = {};
    this.marker = new GMarker(coords, icon);
    this.contents = '';
    this.currentCaseType;
    this.opened = false;

    this.getCaseTypeId = function(casetype)
    {
        return 'casetype-' + casetype + '-' + latLngToString(this.coords).toLowerCase();
    }

    this.getContents = function()
    {
        return this.contents;
    }

    /**
     * Rebuilds the HTML contents.
     * Call this function when new cases are added, or case details have been updated.
     *
     * @return {String} A string of html to feed to {@link GMarker.openInfoWindowHtml}.
     */
    this.rebuildContents = function()
    {
        this.contents = '<div class="cases-list" style="max-width: ' + FilterMap.infoWindowWidth + 'px;"><ul>';

        for (var type in this.cases) {

            var type_id = this.getCaseTypeId(type);
            this.contents += '<li class="casetype-' + type + '" id="' + type_id + '">' + this.cases[type]['title'] + '<ul class="nodisplay">';

            for (var id in this.cases[type]['cases']) {
                this.contents += '<li class="' + this.cases[type]['cases'][id].status + '"><a href="' + this.cases[type]['cases'][id].uri + '"><span class="date">' + this.formatDate(this.cases[type]['cases'][id].date) + '</span></a></li>';
            }

            this.contents += '</ul></li>';

        }
        this.contents += '</ul>';
        this.contents += '<span class="case-disclaimer"> ' + getText('DCPPrivacyDisclaimer') + '</span>';
        this.contents += '</div>';

        return this.contents;
    }

    this.bindContents = function()
    {
        this.marker.bindInfoWindowHtml(this.contents, {'maxWidth' : FilterMap.infoWindowWidth, 'maxHeight' : FilterMap.infoWindowHeight});
    }

    this.selectCaseType = function(li)
    {
        if (li) {
            if (this.currentCaseType && (this.currentCaseType !== li)) {
                $(this.currentCaseType).removeClass('selected');
            }

            this.currentCaseType = li;
            $(this.currentCaseType).addClass('selected');
        }
    }

    /**
     * Sets the <code>opened</code> property of a marker.
     *
     * @param {Boolean} opened
     */
    this.toggleInfoWindow = function(e, opened)
    {
        this.opened = opened;

        if (this.opened) {
            var self = this;
            $('.cases-list li').each(function() { $(this).unbind('click').bind('click', function() { self.selectCaseType(this); }); });

            // select the first type
            this.selectCaseType($('.cases-list li').get(0));

            // move map to this marker
            // the variable 'map' is a global variable, typically a FilterMap instance
            if (map && (map instanceof FilterMap)) {
                var yoffset = 100;
                var point = map.gMap.fromLatLngToDivPixel(this.coords);
                var centerPoint = map.gMap.fromLatLngToDivPixel(map.gMap.getCenter());
                centerPoint.x -= (centerPoint.x - point.x);
                centerPoint.y -= (centerPoint.y + 150 - point.y);
                var latlng = map.gMap.fromDivPixelToLatLng(centerPoint);
                map.gMap.panTo(latlng);
            }

            // assume a filtermap is instanciated with variable 'filtermap'
            map.filters['coords'] = latLngToString(this.coords);

        } else {

            delete map.filters['coords'];

        }

        updateDocumentLocation(map.filters);
    }

    /**
     * Adds a case to the marker. Different types of cases will be grouped.
     * After adding the case, {@link rebuildContents} is automatically invoked to refresh the window's contents.
     */
    this.addCase = function(id, number, type, date, status, description, uri, publicarea)
    {
        if (arguments.length != arguments.callee.length) throw new Error(';CasesMarker.addCase : not enough arguments specified.');

        // add new case or overwrite previously added case, either way: rebuild the case data
        if (!this.cases[type['name']]) this.cases[type['name']] = {'title' : type['title'], 'cases' : {}};

        // add the case
        this.cases[type['name']]['cases'][id] = {'id' : id, 'number' : number, 'date' : date, 'status' : status, 'description' : description, 'uri' : uri, 'publicarea' : publicarea};
    }

    /**
     * Clears the cases for this marker
     */
    this.removeCases = function()
    {
        this.cases = {};
    }

    /**
     * Formats a date to a (Dutch) readable string.
     */
    this.formatDate = function(date)
    {
        return zerofill(date.getDate()) + '-' + zerofill(date.getMonth() + 1) + '-' + date.getFullYear();
    }

    // add events to the marker
    GEvent.addListener(this.marker, "infowindowopen", Delegate.create(this, this.toggleInfoWindow, [true]));
    GEvent.addListener(this.marker, "infowindowclose", Delegate.create(this, this.toggleInfoWindow, [false]));
}

var Modal = {
    'load' : function()
    {
        $('#body').after('<div id="modal-overlay" class="nodisplay"></div><div id="modal" class="modal nodisplay"></div>');
    },

    'display' : function(message, type)
    {
        $('#modal').attr('class', 'modal ' + type).html(message + '<p class="modal-title"><span class="closemodal">X</span></p>');
        $('.closemodal').bind('click', Modal.close);

        $('#modal').removeClass('nodisplay');

        if (type == 'error') {
            $('#modal-overlay').removeClass('nodisplay');
        }
    },

    'close' : function()
    {
        $('#modal').addClass('nodisplay');
        $('#modal-overlay').addClass('nodisplay');
    }
};

function updateDocumentLocation(params)
{
    var queryparams = [];
    for (var a in params) queryparams.push(a + '=' + params[a]);


    var url = document.location.protocol + '//' + document.location.hostname + document.location.pathname + '#' + queryparams.join('&');
    document.location.href = url;
}

function latLngToString(latlng)
{
    return new String(latlng.lat().toString() + latlng.lng().toString()).replace(/\./g, '');
}


/**
 *
 */
window.passwordInputs = [];

function addPasswordStrengthMeters()
{
    if (window.passwordInputs.length > 0) {
        var pwd;
        for (var i = 0; i <window.passwordInputs.length; i++) {
            pwd = $('#' + window.passwordInputs[i]);
            pwd.after('<br/><p class="password-strength">' + getText('MyPasswordStrength') + '</p>');
            pwd.bind('keyup', function() { checkPasswordStrength(pwd); });
        }
    }
}

/**
 * Checks the strength of a password, based on letters, numbers, special characters and combo's
 *
 * @author Steve Moitozo
 * @see http://www.geekwisdom.com/dyn/passwdmeter
 */
function checkPasswordStrength(input)
{
    var verdict = '';
    var className = '';
    var passwd = input.val();
    var score = 0;

    if (passwd == '') {

        verdict = '';
        className = '';

    } else {

        // length
        if (passwd.length < 5) {
            score += 3
        } else if (passwd.length > 4 && passwd.length < 8) {
            score += 6;
        } else if (passwd.length > 7) {
            score += 12;
        }

        // letters
        if (passwd.match(/[a-z]+/)) {
            // at least one lower case letter
            score += 1;
        }
        if (passwd.match(/[A-Z]+/)) {
            // at least one upper case letter
            score += 5;
        }

        // numbers
        if (passwd.match(/\d+/)) {
            // at least one number
            score += 5;
        }

        // special characters
        if (passwd.match(/.[!,@,#,$,%,^,&,*,?,_,~]/)) {
            // at least one special character
            score += 5;
        }

        // combo's
        if (passwd.match(/([a-z].*[A-Z])|([A-Z].*[a-z])/)) {
            // both upper and lower case
            score += 2;
        }
        if (passwd.match(/([a-zA-Z])/) && passwd.match(/([0-9])/)) {
            // both letters and numbers
            score += 2;
        }
        if (passwd.match(/([a-zA-Z0-9].*[!,@,#,$,%,^,&,*,?,_,~])|([!,@,#,$,%,^,&,*,?,_,~].*[a-zA-Z0-9])/)) {
            // letters, numbers, and special characters
            score += 2;
        }

        if (score < 6) {
           verdict = getText('MyPasswordStrengthVeryWeak');
           className = 'password-very-weak';
        } else if (score < 13) {
           verdict = getText('MyPasswordStrengthWeak');
           className = 'password-weak';
        } else if (score < 24) {
           verdict = getText('MyPasswordStrengthAverage');
           className = 'password-average';
        } else if (score < 33) {
           verdict = getText('MyPasswordStrengthStrong');
           className = 'password-strong';
        } else {
           verdict = getText('MyPasswordStrengthStronger');
           className = 'password-very-strong';
        }
    }

    input.parent().find('.password-strength').html(getText('MyPasswordStrength') + ' <span class="' + className + '">' + verdict + '</span>');
}

function zerofill(x)
{
    return new String(((x < 0) || (x > 9) ? '' : '0') + x);
}
