/*!
 * insert license here
 */
/*
 * file: common.js
 *
 * This file includes code common to all parts of the LL-MAP application
 */

/*jslint browser: true, devel: true, eqeqeq: false, onevar: false, plusplus: false, white: false */
/*global Ext: true, LLMAP: true, OpenLayers: true, window: true */

Ext.QuickTips.init();

Ext.Ajax.disableCaching            = false;
Ext.Ajax.timeout                   = 120*1000; // 2 minutes
Ext.layout.FormLayout.labelStyle   = 'font-weight: bold;';
Ext.Updater.defaults.loadScripts   = true;

Ext.namespace('LLMAP.UI');

// Convert a decimal degrees value into a string in degrees/minutes/seconds format

LLMAP.degreesToDMS = function (val) {
    var d = Math.floor(val);
    var m = Math.floor(val = (val - d) * 60);
    var s = Math.floor(val = (val - m) * 6000);

    s = s / 100.00;

    return "" + d + "\u00B0 " + m + "\u0027 " + s + "\u0022";
};

LLMAP.formatLat = function (val) {
    var dir = val > 0? 'N' : 'S';

    return LLMAP.degreesToDMS(Math.abs(val)) + ' ' + dir;
};

LLMAP.formatLon = function (val) {
    var dir = val > 0? 'E' : 'W';

    return LLMAP.degreesToDMS(Math.abs(val)) + ' ' + dir;
};

LLMAP.UI.LoginWindow = Ext.extend(Ext.Window, {
    initComponent: function () {
        Ext.apply(this, {
            layout: 'fit',
            title: 'Login to LL-MAP',
            modal: true,
            border: false,
            height: 215,
            width: 300, 
            items: {
                xtype: 'form',
                ref: 'form_panel',
                bodyStyle: 'padding: 10px;',
                items: [
                    {
                        xtype: 'displayfield',
                        hideLabel: true,
                        cls: 'login-window-msg',
                        value: 'Please log in with your LL-MAP account in order to continue.'
                    },
                    {
                        xtype: 'textfield',
                        name: 'login',
                        fieldLabel: 'User ID',
                        width: 150
                    },
                    {
                        xtype: 'textfield',
                        name: 'password',
                        inputType: 'password',
                        fieldLabel: 'Password',
                        width: 150
                    },
                    {
                        xtype: 'displayfield',
                        name: 'message',
                        hideLabel: true,
                        cls: 'login-window-error',
                        value: ''
                    }
                ],
                keys: [
                    {
                        key: [ Ext.EventObject.ENTER ],
                        scope: this,
                        handler: this.submitLogin
                    }
                ],
                buttons: [
                    {
                        text: 'Log In',
                        scope: this,
                        handler: this.submitLogin
                    },
                    {
                        text: 'Cancel',
                        scope: this,
                        handler: function () {
                            this.hide();

                            this.fireEvent('loginfailure');
                        }
                    }
                ]
            },
            listeners: {
                scope: this,
                show: function () {
                    this.form_panel.getForm().findField('login').focus(false, 25);
                }
            }
        });

        LLMAP.UI.LoginWindow.superclass.initComponent.call(this);

        this.addEvents('loginsuccess', 'loginfailure');
    },

    submitLogin: function () {
        var msg;

        this.form_panel.getForm().submit({
            clientValidation: false,
            url: '/account/login-action.json',
            scope: this,
            success: function (form, action) {
                this.hide();

                this.fireEvent('loginsuccess', action.result.data);
            },
            failure: function (form, action) {
                switch (action.failureType) {
                    case Ext.form.Action.CONNECT_FAILURE:
                        msg = 'Unable to communicate with the server. Please try again in a few minutes.';
                        break;
                    case Ext.form.Action.SERVER_INVALID:
                        msg = action.result.message;
                        break;
                    default:
                        msg = 'Unknown Error';
                        break;
                }

                this.form_panel.getForm().findField('message').setValue(msg);
            }
        });
    }
});

var LoginWindow    = new LLMAP.UI.LoginWindow();
var ExtConnRequest = Ext.data.Connection.prototype.request;

LLMAP.promptForLogin = false;

/*
 * Override Ext.data.Connection to catch errors in an intelligent manner.
 *
 * For 401 errors (Not Authorized) we optionally display the login window.
 * If the user * then logs in successfully the failed request is resubmitted.
 * Otherwise the failure is pased back to the caller.
 *
 * For all other errors we display an error alert. TODO: maybe allow the users
 * to retry those requests too.
 */

Ext.override(Ext.data.Connection, {
   request: function (options) {
       var failure = options.failure;
       var success = options.success;
       var conn    = this;

/*
       options.success = function (response, options) {
           if (ajax_error_banner) {
               ajax_error_banner.style.display = 'none';
           }
           if (success)
               success.call (options.scope, response, options);
       };
*/
       options.failure = function (response, options) {
            if (response.status == 401) {
                if (LLMAP.promptForLogin === true) {
                    LoginWindow.show();

                    LoginWindow.addListener('loginsuccess', function () {
                        conn.request(options);
                    }, this, { single: true });

                    LoginWindow.addListener('loginfailure', function () {
                        if (failure) {
                            failure.call(options.scope, response, options);
                        }
                    }, this, { single: true });
                }
                else {
                    Ext.MessageBox.show({
                        title: 'Error',
                        msg: 'Sorry, you do not have access rights to the information you have requested.',
                        buttons: Ext.MessageBox.OK,
                        icon: Ext.MessageBox.ERROR,
                        width: 300
                    });

                    if (failure) {
                        failure.call(options.scope, response, options);
                    }
                }
            }
            else {
                var body = response.responseText;
                var ct   = response.getResponseHeader('Content-Type');
                var msg;

                if (ct.match(/^application\/json/)) {
                    body = Ext.decode(body);

                    msg = body.message;
                }
                else {
                    msg = body;
                }

                Ext.MessageBox.show({
                    title: 'Error',
                    msg: msg,
                    buttons: Ext.MessageBox.OK,
                    icon: Ext.MessageBox.ERROR,
                    width: 600
                });

                if (failure) {
                    failure.call(options.scope, response, options);
                }
            }
       };

       ExtConnRequest.call(this, options);
   }
});

LLMAP.openHelpViewer = function(section, topic) {
    var w = window.open('', 'helpViewer', 'status=no,toolbar=no,location=no,height=700,width=900');
    var q;

    if (topic === undefined) {
        topic = 'top';
    }

    if ((w !== null) && !w.closed && (w.helpViewer !== undefined)) {
        w.helpViewer.goToTopic(section, topic);
    }
    else {
        window.open('/help.html#' + section + '/' + topic, 'helpViewer');
    }
};

LLMAP.openMapViewer = function(ref) {
    var w = window.open('', 'mapViewer', 'status=no,toolbar=no,location=no,height=700,width=900');
    var q;

    if (ref !== undefined) {
        q = '?maps=' + ref;
    }
    else {
        q = '';
    }

    if ((w !== null) && !w.closed && (w.mapViewer !== undefined)) {
        w.mapViewer.loadMap(ref);
    }
    else {
        window.open('/viewer.html' + q, 'mapViewer');
    }
};

LLMAP.CountrySelector = Ext.extend(Ext.form.ComboBox, {
    displayField: 'primary_name',
    valueField: 'code_a3',
    triggerAction: 'all',
    hideTrigger: false,
    forceSelection: true,
    fieldLabel: 'Country Name',
    typeAhead: false,
    editable: false,
    mode: 'remote',

    initComponent: function () {
        Ext.applyIf(this, {
            store: LLMAP.Country.zfCreateStore()
        });

        LLMAP.CountrySelector.superclass.initComponent.call(this);
    }
});

LLMAP.CodeSelector = Ext.extend(Ext.form.ComboBox, {
    displayField: 'primary_name',
    valueField: 'code',
    itemSelector: 'div.language-selector-item',
    hideTrigger: true,
    forceSelection: true,
    emptyText: 'Type a language name...',
    fieldLabel: 'Language Name',
    typeAhead: false,
    minChars: 2,
    queryParam: 'name',
    codeType: 'language',

    initComponent: function () {
        Ext.applyIf(this, {
            tpl: new Ext.XTemplate(
                '<tpl for=".">',
                    '<div class="language-selector-item">',
                        '<tpl if="standard_name = \'Private Use\'">',
                            '<span>{primary_name} ({code})</span>',
                        '</tpl>',
                        '<tpl if="standard_name != \'Private Use\'">',
                            '<span style="text-weight: bold">{primary_name} ({code})</span>',
                        '</tpl>',
                    '</div>',
                '</tpl>'
            ),
            store: LLMAP.Code.zfCreateStore({
                baseParams: {
                    codeType: this.codeType
                }
            })
        });

        LLMAP.CodeSelector.superclass.initComponent.call(this);
    }
});

LLMAP.UI.YesNoSelection = Ext.extend(Ext.form.ComboBox, {
    store: LLMAP.types.yes_no,
    valueField: 'id',
    displayField: 'description',
    forceSelection: true,
    typeAhead: false,
    mode: 'local',
    triggerAction: 'all',
    selectOnFocus: true,
    width: 200
});


LLMAP.UI.LoadingIndicator = function () {
    this.container = Ext.get(document.body);

    this.el = Ext.DomHelper.append(this.container, {
        tag: 'div',
        style: {
            background: 'black',
            border: '1px solid white',
            height: 108,
            margin: 0,
            padding: 0,
            position: 'absolute',
            width: 108,
            'z-index': 99999
        },
        children: [
            {
                tag: 'img',
                src: '/images/ajax-loader.gif',
                style: {
                    border: 'none',
                    height: 100,
                    margin: 0,
                    padding: '4px',
                    width: 100 
                }
            }
        ]
    }, true);

    this.el.setVisible(false);
};

LLMAP.UI.LoadingIndicator.prototype.hide = function () {
    this.el.setVisible(false);
    this.container.unmask();
};

LLMAP.UI.LoadingIndicator.prototype.show = function () {
    var mask = this.container.mask();
    mask.setStyle('z-index', 50000);

    this.el.alignTo(this.container, 'c-c', false);
    this.el.setVisible(true);
};
/*
 * LLMAP.UI.MapLayer
 * Written by Joshua M. Thompson <joshua@linguistlist.org>
 *
 * This class acts as a glue that turns a single LL-MAP map
 * (which may include multiple layers) into a single OpenLayers
 * map layer.
 */

/*jslint browser: true, devel: true, eqeqeq: false, onevar: false, plusplus: false, white: false */
/*global Ext: true, LLMAP: true, OpenLayers: true */

LLMAP.UI.MapLayer = Ext.extend(Ext.util.Observable, {
    url: null,
    service: null,

    layers: [],

    srs: [ 'EPSG:4326' ],

    initialOpacity: 0.70,

    enabled: true,

    isOverlay: false,
    show_legend: true,
    visible: true,

    temporal: false,
    start_year: null,
    end_year: null,
    year_increment: 1,
    current_year: null,

    constructor: function (config) {
        LLMAP.UI.MapLayer.superclass.constructor.call(this, config);

        Ext.apply(this, config);

        this.addEvents('setvisible', 'update');

        this.map_layer = new OpenLayers.Layer.WMS(
            this.name,
            this.getServiceURL(),
            {
                layers: this.getLayerList(),
                styles: this.getStyleList(),
                cql_filter: this.getFilterList(),
                format: this.format,
                tiled: false,
                transparent: true
            },
            {
                ratio: 1,
                displayOutsideMaxExtent: true,
                singleTile: true
            }
        );

        this.map_layer.setOpacity(this.initialOpacity);

        this.current_year = this.start_year;
    },

    getFilterList: function () {
        var list = [];

        for (var i = 0 ; i < this.layers.length ; i++) {
            var layer = this.layers[i];

            if (layer.visible) {
                var filter = layer.filter;

                if (filter === undefined) {
                    filter = 'INCLUDE';
                }

                list.push(filter);
            }
        }

        return list.join(';');
    },

    getLegendGraphic: function (layer, scale) {
        var url = this.getServiceURL() + 'SERVICE=WMS&VERSION=1.1.1&REQUEST=GetLegendGraphic&FORMAT=image/png&LAYER=' + layer.id;

        if (layer.styleName) {
            url = url + '&STYLE=' + layer.styleName;
        }

        if (this.legend_options) {
            url = url + '&LEGEND_OPTIONS=' + this.legend_options.join(';');
        }

/*
        if (scale != null) {
            url = url + '&SCALE=' + scale;
        }
*/

        url = url + '&llrand=' + Math.random();

        return url;
    },

    getLayerList: function () {
        var list = [];

        for (var i = 0 ; i < this.layers.length ; i++) {
            var layer = this.layers[i];

            if (layer.visible && layer.in_range) {
                list.push(layer.id);
            }
        }

        return list.join(',');
    },

    getStyleList: function () {
        var list = [];

        for (var i = 0 ; i < this.layers.length ; i++) {
            var layer = this.layers[i];

            if (layer.visible) {
                list.push(layer.styleName);
            }
        }

        return list.join(',');
    },

    getVisibleLayers: function () {
        var list = [];

        for (var i = 0 ; i < this.layers.length ; i++) {
            var layer = this.layers[i];

            if (layer.visible) {
                list.push(layer.id);
            }
        }

        return list;
    },

    isSupportedProjection: function (srs) {
        return (this.srs.indexOf(srs) >= 0);
    },

    setEnabled: function (enabled) {
        this.fireEvent('setvisible', this, this.visible && this.enabled, this.visible && enabled);

        this.enabled = enabled;

        this.updateLayers();
    },

    setVisible: function (visible) {
        this.fireEvent('setvisible', this, this.visible && this.enabled, visible && this.enabled);

        this.visible = visible;

        this.updateLayers();
    },

    setYear: function (year) {
        this.current_year = year;

        this.updateLayers();
    },

    updateLayers: function () {
        for (var i = 0 ; i < this.layers.length ; i++) {
            var layer = this.layers[i];
            var ms    = this.current_year - this.year_increment;
            var me    = this.current_year + this.year_increment;

            if (this.temporal) {
                layer.in_range = (layer.start_year >= ms) && (layer.end_year <= me);
            }
            else {
                layer.in_range = true;
            }
        }

        var layers = this.getLayerList();

        if (!this.enabled || (layers === '')) {
            this.map_layer.setVisibility(false);
        }
        else {
            this.map_layer.mergeNewParams({
                layers: layers,
                styles: this.getStyleList(),
                cql_filter: this.getFilterList(),
                llrand: Math.random() // force reload
            });

            this.map_layer.setVisibility(this.visible);
        }

        this.fireEvent('update', this);
    }
});

LLMAP.UI.StdMapLayer = Ext.extend(LLMAP.UI.MapLayer, {
    format: 'image/png8',

    getServiceURL: function () {
        return this.url;
    },

    getStyleList: function () {
        return '';
    }
});

LLMAP.UI.WMSMapLayer = Ext.extend(LLMAP.UI.MapLayer, {
    format: 'image/png',

    getServiceURL: function () {
        return this.url;
    }
});
/*
 * LLMAP.UI.Map
 * Written by Joshua M. Thompson <joshua@linguistlist.org>
 *
 * This class encapsulates the functionality of a 'service',
 * which is a group of related layers that together make up
 * a map.
 */

/*jslint browser: true, devel: true, eqeqeq: false, onevar: false, plusplus: false, white: false */
/*global Ext: true, LLMAP: true, OpenLayers: true */

LLMAP.BaseMaps = {
    overlayLayers: [
        {
            id: 'ne14:ne_10m_admin_0_countries',
            name: 'Country Border',
            show_legend: true,
            legend_title: false,
            visible: true
        },
        {
            id: 'ne14:ne_10m_admin_1_states_provinces_scale_ranks',
            name: 'State/Province Border',
            show_legend: true,
            legend_title: false,
            visible: false
        },
        {
            id: 'ne14:ne_10m_geography_regions_polys',
            name: 'Geographic Regions',
            show_legend: true,
            legend_title: true,
            visible: true
        }/*,
        {
            id: 'ne14:ne_10m_geography_regions_points',
            name: 'Geographic Regions',
            show_legend: true,
            legend_title: true,
            visible: true
        }*/
    ],

    srsList: [
        [ 'EPSG:4326', 'WGS 1984'           ],
        [ 'EPSG:3857', 'Spherical Mercator' ]
    ],

    srsParams: {
        'EPSG:4326': {
            units: 'degrees',
            numZoomLevels: 16,
            maxResolution: 0.703125,
            maxExtent: new OpenLayers.Bounds(-180, -90, 180, 90),
            resolutions: [
                0.703125,
                0.3515625,
                0.17578125,
                0.087890625,
                0.0439453125,
                0.02197265625,
                0.010986328125,
                0.0054931640625,
                0.00274658203125,
                0.001373291015625,
                6.866455078125E-4,
                3.4332275390625E-4,
                1.71661376953125E-4,
                8.58306884765625E-5,
                4.291534423828125E-5,
                2.1457672119140625E-5/*,
                1.0728836059570312E-5,
                5.364418029785156E-6,
                2.682209014892578E-6,
                1.341104507446289E-6,
                6.705522537231445E-7,
                3.3527612686157227E-7,
                1.6763806343078613E-7,
                8.381903171539307E-8,
                4.190951585769653E-8,
                2.0954757928848267E-8,
                1.0477378964424133E-8,
                5.238689482212067E-9,
                2.6193447411060333E-9,
                1.3096723705530167E-9,
                6.548361852765083E-10*/
            ]
        },

        'EPSG:3857': {
            units: 'm',
            numZoomLevels: 16,
            maxResolution: 78271.516953125,
            maxExtent: new OpenLayers.Bounds(-2.003750834E7, -2.003750834E7, 2.003750834E7, 2.003750834E7),
            resolutions: [
                78271.516953125,
                39135.7584765625,
                19567.87923828125,
                9783.939619140625,
                4891.9698095703125,
                2445.9849047851562,
                1222.9924523925781,
                611.4962261962891,
                305.74811309814453,
                152.87405654907226,
                76.43702827453613,
                38.218514137268066,
                19.109257068634033,
                9.554628534317017,
                4.777314267158508,
                2.388657133579254/*,
                1.194328566789627,
                0.5971642833948135,
                0.29858214169740677,
                0.14929107084870338,
                0.07464553542435169,
                0.037322767712175846,
                0.018661383856087923,
                0.009330691928043961,
                0.004665345964021981,
                0.0023326729820109904,
                0.0011663364910054952,
                5.831682455027476E-4,
                2.915841227513738E-4,
                1.457920613756869E-4*/
            ]
        }
    }
};

LLMAP.BaseMaps.BaseMap = Ext.extend(Ext.util.Observable, {
    id: null,
    name: null,
    format: 'image/png8',

    overlayStyles: [],

    constructor: function (config) {
        LLMAP.BaseMaps.BaseMap.superclass.constructor.call(this, config);

        Ext.apply(this, config);

        this.setProjection('EPSG:4326');
    },

    setProjection: function (srs) {
        var options = {
            isBaseLayer: true,
            projection: new OpenLayers.Projection(srs),
            transitionEffect: 'resize',
            wrapDateLine: true
        };

        Ext.apply(options, LLMAP.BaseMaps.srsParams[srs]);

        options.maxExtent = options.maxExtent.clone();

        this.base_layer = new OpenLayers.Layer.WMS(
            this.name,
            'http://barbara.linguistlist.org/geoserver/gwc/service/wms',
            {
                layers: this.id,
                format: this.format,
                tiled: true
            },
            options
        );
    }
});

LLMAP.BaseMaps.Satellite = Ext.extend(LLMAP.BaseMaps.BaseMap, {
    id: 'blue-marble-500m',
    name: 'Satellite',
    format: 'image/jpeg',

    overlayStyles: [
        'sbm-admin0-boundary',
        'sbm-admin1-boundary',
        'vbm-geography-regions-polys',
        'vbm-geography-regions-points'
    ]
});

LLMAP.BaseMaps.Vector = Ext.extend(LLMAP.BaseMaps.BaseMap, {
    id: 'vector-base-map',
    name: 'Map',
    format: 'image/png8',

    overlayStyles: [
        'vbm-admin0-boundary',
        'vbm-admin1-boundary',
        'vbm-geography-regions-polys',
        'vbm-geography-regions-points'
    ]
});

LLMAP.BaseMaps.DefaultOverlay = Ext.extend(LLMAP.UI.WMSMapLayer, {
    id: 'default-overlay',
    name: 'Base Map Features',
    url: 'http://barbara.linguistlist.org/geoserver/wms?',
    isOverlay: true,

    srs: [ 'EPSG:4326', 'EPSG:3857' ],

    legend_options: [ 'forceLabels:on', 'fontName:Arial Unicode MS', 'fontSize:12', 'fontStyle:normal', 'fontAntiAliasing:true' ],

    constructor: function (config) {
        config.layers = [];

        for (var i = 0 ; i < LLMAP.BaseMaps.overlayLayers.length ; i++) {
            var layer = LLMAP.BaseMaps.overlayLayers[i];

            config.layers.push({
                id: layer.id,
                name: layer.name,
                styleName: '',
                show_legend: layer.show_legend,
                legend_title: layer.legend_title,
                visible: layer.visible
            });
        }

        if (typeof config.defaultVisibility == 'object') {
            for (var i = 0 ; i < config.layers.length ; i++) {
                config.layers[i].visible = config.defaultVisibility[i]? true : false; // force true/false
            }
        }

        LLMAP.BaseMaps.DefaultOverlay.superclass.constructor.call(this,  config);
    },

    changeBaseMap: function (basemap) {
        for (var i = 0 ; i < this.layers.length ; i++) {
            this.layers[i].styleName = basemap.overlayStyles[i];
        }

        this.fireEvent('update', this);
    }
});
/*
 * LLMAP.UI.ContactForm
 * Written by Joshua M. Thompson <joshua@linguistlist.org>
 *
 * This class provides an Ext.Panel for allowing the user
 * to send a message to the LL-MAP team.
 */

/*jslint browser: true, devel: true, eqeqeq: false, onevar: false, plusplus: false, white: false */
/*global Ext: true, LLMAP: true, OpenLayers: true */

LLMAP.UI.ContactForm = Ext.extend(Ext.Window, {
    layout: 'fit',
    title: 'Contact LL-MAP',
    closable: false,
    hidden: false,
    modal: true,
    width: 475,
    height: 450,

    subject: '',
    message: '',

    initComponent: function () {
        Ext.apply(this, {
            items: [
                {
                    xtype: 'form',
                    border: false,
                    bodyStyle: 'padding: 10px',
                    labelAlign: 'right',
                    labelWidth: 100,
                    labelPadding: 5,
                    monitorValid: true,
                    items: [
                        {
                            xtype: 'textfield',
                            name: 'email',
                            fieldLabel: 'Your Email',
                            width: 320,
                            allowBlank: false,
                            emptyText: 'Please enter your email address',
                            blankText: 'You must provide your email address',
                            vtype: 'email'
                        },
                        {
                            xtype: 'textfield',
                            name: 'name',
                            fieldLabel: 'Your Name',
                            width: 320,
                            allowBlank: false,
                            emptyText: 'Please enter your full name',
                            blankText: 'You must provide your full name'
                        },
                        {
                            xtype: 'textfield',
                            name: 'institution',
                            fieldLabel: 'Affiliation',
                            width: 320,
                            allowBlank: true
                        },
                        {
                            xtype: 'textfield',
                            name: 'subject',
                            fieldLabel: 'Subject',
                            width: 320,
                            allowBlank: false,
                            emptyText: 'Please enter a subject',
                            blankText: 'You must enter a subject for your message',
                            value: this.subject
                        },
                        {
                            xtype: 'textarea',
                            name: 'msgbody',
                            fieldLabel: 'Message',
                            height: 125,
                            width: 320,
                            allowBlank: false,
                            emptyText: 'Please enter your reason for contacting us',
                            blankText: 'You must provide a message',
                            value: this.message
                        },
                        {
                            xtype: 'recaptcha',
                            name: 'recaptcha',
                            fieldLabel: 'Verification',
                            id: 'recaptcha',
                            publickey: '6LeTzrkSAAAAAATl1zNpgcXT6MwRHp9Ri5gnJNme',
                            lang: 'en',
                            theme: 'white'
                        }
                    ],
                    listeners: {
                        scope: this,
                        clientvalidation: function (fp, valid) {
                            this.buttons[0].setDisabled(!valid);
                        }
                    }
                }
            ],
            buttons: [
                {
                    text: 'Send Message',
                    scope: this,
                    handler: function () {
                        this.sendMessage();
                    }
                },
                {
                    text: 'Cancel',
                    scope: this,
                    handler: function () { this.close(); }
                }
            ]
        });

        LLMAP.UI.ContactForm.superclass.initComponent.call(this);
    },

    sendMessage: function () {
        this.getComponent(0).getForm().submit({
            url: '/contact.json',
            clientValidation: true,
            submitEmptyText: false,
            waitMsg: 'Sending Message...',
            scope: this,
            success: function (form, action) {
                this.close();

                Ext.Msg.alert('Success', 'Your message has been sent. We appreciate your feedback.');
            },
            failure: function (form, action) {
                if (action.failureType === Ext.form.Action.CONNECT_FAILURE) {
                    Ext.Msg.alert('Error', 'Status:'+action.response.status+': '+ action.response.statusText);
                }
                else if (action.failureType === Ext.form.Action.SERVER_INVALID) {
                    if (action.result.errorMsg) {
                        Ext.Msg.alert('Error', action.result.errorMsg);
                    }
                }
            }
        });
    }
});

/*
 * LLMAP.UI.GetFeatureInfo
 * Written by Joshua M. Thompson <joshua@linguistlist.org>
 *
 * This object class displays information about a map feature by making
 * a WMS GetFeatureInfo call and displaying the resulting HTML inside an
 * OpenLayers.Popup
 */

/*jslint browser: true, devel: true, eqeqeq: false, onevar: false, plusplus: false, white: false */
/*global Ext: true, LLMAP: true, OpenLayers: true */

LLMAP.UI.GetFeatureInfo = function (map_panel) {
    this.id = Ext.id(null, 'feature-info-');
    this.mp = map_panel;
};

LLMAP.UI.GetFeatureInfo.prototype.close = function () {
    if (this.transid) {
        Ext.Ajax.abort(this.transid);

        delete this.transid;
    }

    this.mp.removePopup(this.popup);

    this.popup.destroy();

    delete this.popup;
};

LLMAP.UI.GetFeatureInfo.prototype.show = function (imagePos, lonLat) {
    var config = this.mp.getMapConfig();
    var disp   = lonLat.clone();

    disp.transform(this.mp.getProjectionObject(), this.mp.getDisplayProjection());

    this.top = '<div class="feature-info">' +
               '<h1>Features near ' + LLMAP.formatLat(disp.lat) + ', ' + LLMAP.formatLon(disp.lon) + '</h1>' +
               '<div class="feature-info-inner">';

    this.bottom = '</div></div>';

    if (this.popup) {
        this.close();
    }

    this.popup = new OpenLayers.Popup.FramedCloud(
        this.id,
        lonLat,
        new OpenLayers.Size(400, 300),
        this.top + this.bottom,
        null,
        true,
        this.close.createDelegate(this)
    );

    this.popup.autoSize          = false;
    this.popup.panMapIfOutOfView = true;

    this.mp.addPopup(this.popup);

    config.x   = imagePos.x;
    config.y   = imagePos.y;
    config.lon = lonLat.lon;
    config.lat = lonLat.lat;

    this.transid = Ext.Ajax.request({
        url: '/search/get_features.html',
        method: 'POST',
        jsonData: config,
        scope: this,
        failure: function () {},
        success: function (response, options) {
            this.popup.setContentHTML(this.top + response.responseText + this.bottom);
        }
    });
};
/*
 * LLMAP.UI.Legend
 * Written by Joshua M. Thompson <joshua@linguistlist.org>
 *
 * This class implements a map legend as a BoxComponent.
 * It monitors the map to which it is attached for map or
 * layer modifications and updates itself accordingly.
 */

/*jslint browser: true, devel: true, eqeqeq: false, onevar: false, plusplus: false, white: false */
/*global Ext: true, LLMAP: true, OpenLayers: true */

LLMAP.UI.Legend = Ext.extend(Ext.BoxComponent, {
    map_panel: null,
    legends: [],

    workbenchMode: false,

    initComponent: function () {
        this.addEvents('removemap', 'showcredits', 'zoommap');
    },

    attach: function (map_panel) {
        this.map_panel = map_panel;

        map_panel.addListener({
            scope: this,
            addmap: this.handleAddMap,
            mapstatechange: this.handleMapStateChange,
            removemap: this.handleRemoveMap
        });
    },

    findLegend: function (map) {
        var el;

        for (var i = 0 ; i < this.legends.length ; i++) {
            if (this.legends[i].map === map) {
                return this.legends[i];
            }
        }

        return null;
    },

    handleAddMap: function (map_panel, map) {
        map.addListener('setvisible', this.handleSetVisible, this);
        map.addListener('update',     this.handleUpdate,     this);

        if (map.show_legend) {
            var legendSpec = { tag: 'div', cls: 'legend-map' };
            var legendEl;

            if (map.isOverlay) {
                legendEl = Ext.DomHelper.insertFirst(this.body, legendSpec, true);

                this.overlayEl = legendEl;
            }
            else if (this.overlayEl) {
                legendEl = Ext.DomHelper.insertAfter(this.overlayEl, legendSpec, true);
            }
            else {
                legendEl = Ext.DomHelper.insertFirst(this.body, legendSpec, true);
            }

            legendEl.setVisibilityMode(Ext.Element.DISPLAY);

            var header = Ext.DomHelper.append(legendEl, {
                tag: 'div',
                cls: 'legend-map-title',
                html: map.name
            }, true);

            if (!map.isOverlay) {
                var controls = Ext.DomHelper.append(header, { tag: 'div', cls: 'legend-map-controls' }, true);

                if (!this.workbenchMode) {
                    var ctlAbout = Ext.DomHelper.append(controls, {
                        tag: 'img',
                        cls: 'legend-map-control',
                        src: '/icons/silk/information.png'
                    }, true);

                    ctlAbout.on('click',  function () { this.fireEvent('showcredits', map); }, this);

                    Ext.QuickTips.register({ target: ctlAbout, text: 'About This Map'   });
                }

                var ctlZoom = Ext.DomHelper.append(controls, {
                    tag: 'img',
                    cls: 'legend-map-control',
                    src: '/icons/silk/zoom.png'
                }, true);

                Ext.QuickTips.register({ target: ctlZoom, text: 'Zoom to Bounds'   });

                ctlZoom.on('click',   function () { this.fireEvent('zoommap',     map); }, this);

                var ctlSetTrans = Ext.DomHelper.append(controls, {
                    tag: 'img',
                    cls: 'legend-map-control',
                    src: '/icons/silk/eye.png'
                }, true);

                Ext.QuickTips.register({ target: ctlSetTrans, text: 'Set Transparency' });

                ctlSetTrans.on('click', function () {
                    var w = new LLMAP.UI.TransparencyControl({ map: map });

                    w.show();
                }, this);

                if (!this.workbenchMode) {
                    var ctlRemove = Ext.DomHelper.append(controls, {
                        tag: 'img',
                        cls: 'legend-map-control',
                        src: '/icons/silk/delete.png'
                    }, true);

                    Ext.QuickTips.register({ target: ctlRemove,   text: 'Remove Map'       });

                    ctlRemove.on('click', function () { this.fireEvent('removemap',   map); }, this);
                }
            }

            var scale = this.map_panel.olmap.getScale();

            var legend = {
                el: legendEl,
                map: map,
                layers: []
            };

            this.legends.push(legend);

            for (var i = 0 ; i < map.layers.length ; i++) {
                var layer    = map.layers[i];
                var layerEl  = Ext.DomHelper.append(legendEl, { tag: 'div', cls: 'legend-map-layer', style: 'display: none' }, true);
                var headerEl;
                var imageEl;
                var imageUrl;

                layerEl.setVisibilityMode(Ext.Element.DISPLAY);

                if (layer.show_legend) {
                    imageUrl = map.getLegendGraphic(layer, scale);
                }

                if (layer.legend_title || (imageUrl === undefined)) {
                    headerEl = Ext.DomHelper.append(layerEl, {
                        tag: 'h3',
                        cls: 'legend-layer-title',
                        html: layer.name
                    }, true);
                }

                if (imageUrl !== undefined) {
                    imageEl = Ext.DomHelper.append(layerEl, { tag: 'img', cls: 'legend-image', src: imageUrl }, true);
                }

/*
                var toggle;

                if (layer.visible) {
                    toggle = Ext.DomHelper.append(layerEl, {
                        tag: 'img',
                        cls: 'legend-layer-toggle',
                        src: '/icons/silk/delete.png'
                    }, true);
                }
                else {
                    toggle = Ext.DomHelper.append(layerEl, {
                        tag: 'img',
                        cls: 'legend-layer-toggle',
                        src: '/icons/silk/add.png'
                    }, true);
                }
*/

                var toggleSpec = {
                    tag: 'input',
                    cls: 'legend-layer-toggle',
                    type: 'checkbox'
                };

                if (layer.visible) {
                    toggleSpec.checked = 'checked';
                }

                var toggle = Ext.DomHelper.append(layerEl, toggleSpec, true);
                toggle.on('click', this.handleToggleLayer.createDelegate(this, [ map, layer ]));

                legend.layers.push({ el: layerEl, header: headerEl, image: imageEl, toggle: toggle });
            }

            this.handleUpdate(map);
        }
    },

    handleMapStateChange: function (mapPanel, map, enabled) {
/*
        this.root.eachChild(function (node) {
            if (node.attributes.map === map) {
                if (enabled) {
                    node.enable();
                    node.setTooltip('');

                    node.eachChild(function (child) { child.enable(); });
                }
                else {
                    node.disable();
                    node.setTooltip('This map is not available in the current map projection');

                    node.eachChild(function (child) { child.disable(); });
                }
            }
        }, this);
*/
    },

    handleRemoveMap: function (map_panel, map) {
        map.removeListener('setvisible', this.handleSetVisible, this);
        map.removeListener('update',     this.handleUpdate,     this);

        if (map.isOverlay) {
            this.overlayEl = null; 
        }

        for (var i = 0 ; i < this.legends.length ; i++) {
            var legend = this.legends[i];

            if (legend.map === map) {
                legend.el.remove();

                this.legends.splice(i, 1);

                break;
            }
        }
    },

    handleSetVisible: function (map, old_visible, visible) {
/*
        var legend = this.findLegend(map);

        if (legend) {
            legend.el.setVisible(visible);
        }
*/
    },

    handleToggleLayer: function (map, layer) {
        if (layer.visible) {
            layer.visible = false;
            }
            else {
            layer.visible = true;
        }

        map.updateLayers();
    },

    handleToggleLegend: function () {
        var t = this.toggle;

        if (this.collapsed) {
            t.removeClass('legend-collapsed');
            t.addClass('legend-expanded');

            this.collapsed = false;

            this.el.setHeight(this.origHeight);
        }
        else {
            t.removeClass('legend-expanded');
            t.addClass('legend-collapsed');

            this.origHeight = this.el.getHeight();

            this.el.setHeight(this.header.getHeight());

            this.collapsed = true;
        }
    },

    handleUpdate: function (map) {
        var legend = this.findLegend(map);

        for (var i = 0 ; i < map.layers.length ; i++) {
            var layer = map.layers[i];

            legend.layers[i].el.setVisible(/*layer.visible && */layer.in_range && (layer.show_legend || layer.legend_title));

            legend.layers[i].toggle.dom.checked = layer.visible;
        }
    },

/*
    handleZoomEnd: function () {
        for (var i = 0 ; i < this.legends.length ; i++) {
            this.handleUpdate(this.legends[i].map);
        }
    },
*/

    onRender: function (container, position) {
        var fragment = {
            tag: 'div',
            cls: 'legend-panel'
        };

        if (position) {
            this.el = Ext.DomHelper.insertBefore(position, fragment, true);
        } else {
            this.el = Ext.DomHelper.append(container, fragment, true);
        }

        if (this.id) {
            this.el.dom.id = this.id;
        }

        this.header = Ext.DomHelper.append(this.el, {
            tag: 'h1',
            html: 'Legend'
        }, true);

        this.toggle = Ext.DomHelper.append(this.el, {
            tag: 'div',
            cls: 'legend-expanded'
        }, true);

        this.body = Ext.DomHelper.append(this.el, {
            tag: 'div',
            cls: 'legend-body'
        }, true);

        this.toggle.addListener({
            scope: this,
            click: this.handleToggleLegend
        });

        this.collapsed = false;
    },

    setHeight: function (height, animate) {
        if (this.collapsed) {
            this.origHeight = height;
        }
        else {
            LLMAP.UI.Legend.superclass.setHeight.call(this, height, animate);

            this.body.setHeight(height - this.header.getHeight());
        }
    }
});
/*
 * LLMAP.UI.LoginPanel
 * Written by Joshua M. Thompson <joshua@linguistlist.org>
 *
 * Login support code
 */

/*jslint browser: true, devel: true, eqeqeq: false, onevar: false, plusplus: false, white: false */
/*global Ext: true, LLMAP: true, OpenLayers: true, window: true */

LLMAP.UI.LoginPanel = Ext.extend(Ext.FormPanel, {
    border: false,

    initComponent: function () {
        Ext.apply(this, {
            bodyStyle: 'padding: 10px',
            buttonAlign: 'center',
            labelWidth: 75,
            labelAlign: 'right',
            defaults: { labelStyle: 'font-weight: bold;' },
            items: [
                {
                    xtype: 'textfield',
                    name: 'login',
                    fieldLabel: 'Login ID',
                    anchor: '100%'
                },
                {
                    xtype: 'textfield',
                    name: 'password',
                    inputType: 'password',
                    fieldLabel: 'Password',
                    anchor: '100%'
                }
            ],
            keys: [
                {
                    key: [ Ext.EventObject.ENTER ],
                    scope: this,
                    handler: this.submitLogin
                }
            ],
            fbar: [
                {
                    text: 'Log In',
                    scope: this,
                    handler: this.submitLogin
                }
            ]
        });

        LLMAP.UI.LoginPanel.superclass.initComponent.call(this);

        this.addEvents('login', 'error');
    },

    submitLogin: function () {
        this.getForm().submit({
            clientValidation: false,
            url: '/account/login-action.json',
            scope: this,
            success: function (form, action) {
                this.fireEvent('login');
            },
            failure: function (form, action) {
                var msg;

                switch (action.failureType) {
                    case Ext.form.Action.CONNECT_FAILURE:
                        msg = 'Unable to communicate with the server. Please try again in a few minutes.';
                        break;
                    case Ext.form.Action.SERVER_INVALID:
                        msg = action.result.message;
                        break;
                    default:
                        msg = 'Unknown Error';
                        break;
                }

                this.fireEvent('error', this, msg);
            }
        });
    }
});


/*
 * LLMAP.UI.AccountRecoveryPanel
 * Written by Joshua M. Thompson <joshua@linguistlist.org>
 *
 * This class provides an Ext.Panel for allowing the user
 * to have new login information sent to their configured
 * email address
 */

/*jslint browser: true, devel: true, eqeqeq: false, onevar: false, plusplus: false, white: false */
/*global Ext: true, LLMAP: true, OpenLayers: true */

LLMAP.UI.AccountRecoveryPanel = Ext.extend(Ext.FormPanel, {
    border: false,
    width: 500,
    height: 200,

    initComponent: function () {
        Ext.apply(this, {
            bodyStyle: 'padding: 10px',
            buttonAlign: 'center',
            labelWidth: 125,
            labelAlign: 'right',
            defaults: { labelStyle: 'font-weight: bold;' },
            items: [
                {
                    xtype: 'textfield',
                    name: 'email',
                    fieldLabel: 'Your Email Address',
                    width: 320
                },
                {
                    xtype: 'recaptcha',
                    name: 'recaptcha',
                    fieldLabel: 'Verification',
                    id: 'recaptcha',
                    publickey: '6LeTzrkSAAAAAATl1zNpgcXT6MwRHp9Ri5gnJNme',
                    lang: 'en',
                    theme: 'white'
                }
            ],
            fbar: [
                {
                    text: 'Submit Request',
                    scope: this,
                    handler: this.submitRequest
                }
            ]
        });

        LLMAP.UI.AccountRecoveryPanel.superclass.initComponent.call(this);

        this.addEvents('success', 'error');
    },

    submitRequest: function () {
        this.getForm().submit({
            clientValidation: false,
            url: '/account/recover-action.json',
            scope: this,
            success: function (form, action) {
                this.fireEvent('success', this);
            },
            failure: function (form, action) {
                var msg;

                switch (action.failureType) {
                    case Ext.form.Action.CONNECT_FAILURE:
                        msg = 'Unable to communicate with the server. Please try again in a few minutes.';
                        break;
                    case Ext.form.Action.SERVER_INVALID:
                        msg = action.result.message;
                        break;
                    default:
                        msg = 'Unknown Error';
                        break;
                }

                this.fireEvent('error', this, msg);
            }
        });
    }
});

LLMAP.UI.LoginWindow = Ext.extend(Ext.Window, {
    initComponent: function () {
        Ext.apply(this, {
            layout: 'fit',
            title: 'Login to LL-MAP',
            modal: true,
            border: false,
            height: 215,
            width: 300, 
            items: {
                xtype: 'form',
                ref: 'form_panel',
                bodyStyle: 'padding: 10px;',
                items: [
                    {
                        xtype: 'displayfield',
                        hideLabel: true,
                        cls: 'login-window-msg',
                        value: 'Please log in with your LL-MAP account in order to continue.'
                    },
                    {
                        xtype: 'textfield',
                        name: 'login',
                        fieldLabel: 'User ID',
                        width: 150
                    },
                    {
                        xtype: 'textfield',
                        name: 'password',
                        inputType: 'password',
                        fieldLabel: 'Password',
                        width: 150
                    },
                    {
                        xtype: 'displayfield',
                        name: 'message',
                        hideLabel: true,
                        cls: 'login-window-error',
                        value: ''
                    }
                ],
                keys: [
                    {
                        key: [ Ext.EventObject.ENTER ],
                        scope: this,
                        handler: this.submitLogin
                    }
                ],
                buttons: [
                    {
                        text: 'Log In',
                        scope: this,
                        handler: this.submitLogin
                    },
                    {
                        text: 'Cancel',
                        scope: this,
                        handler: function () {
                            this.hide();

                            this.fireEvent('loginfailure');
                        }
                    }
                ]
            },
            listeners: {
                scope: this,
                show: function () {
                    this.form_panel.getForm().findField('login').focus(false, 25);
                }
            }
        });

        LLMAP.UI.LoginWindow.superclass.initComponent.call(this);

        this.addEvents('loginsuccess', 'loginfailure');
    },

    submitLogin: function () {
        var msg;

        this.form_panel.getForm().submit({
            clientValidation: false,
            url: '/account/login-action.json',
            scope: this,
            success: function (form, action) {
                this.hide();

                this.fireEvent('loginsuccess', action.result.data);
            },
            failure: function (form, action) {
                switch (action.failureType) {
                    case Ext.form.Action.CONNECT_FAILURE:
                        msg = 'Unable to communicate with the server. Please try again in a few minutes.';
                        break;
                    case Ext.form.Action.SERVER_INVALID:
                        msg = action.result.message;
                        break;
                    default:
                        msg = 'Unknown Error';
                        break;
                }

                this.form_panel.getForm().findField('message').setValue(msg);
            }
        });
    }
});
/*
 * LLMAP.UI.MapPanel
 * Written by Benjamin J. Cool <ben@linguistlist.org>
 *            Joshua M. Thompson <joshua@linguistlist.org>
 *
 * This class provides a map as an Ext.BoxComponent
 */

/*jslint browser: true, devel: true, eqeqeq: false, onevar: false, plusplus: false, white: false */
/*global Ext: true, LLMAP: true, OpenLayers: true */

/*
 * This make OpenLayers recognize the official Spherical Mercator code (EPSG:3857) in addition to addition to the
 * old unofficial one (EPSG:900913). Supposely this will be fixed in OpenLayers 2.11, at which point this hack
 * can be removed.
 */

OpenLayers.Projection.addTransform("EPSG:4326", "EPSG:3857", OpenLayers.Layer.SphericalMercator.projectForward); 
OpenLayers.Projection.addTransform("EPSG:3857", "EPSG:4326", OpenLayers.Layer.SphericalMercator.projectInverse); 

/* Match dpi to WMS spec; OL defaults to 72dpi which is WAY off */
OpenLayers.DOTS_PER_INCH = 25.4 / 0.28;

LLMAP.UI.MapPanel = Ext.extend(Ext.Panel, {
    srs: 'EPSG:4326',

    controls: [],
    maps: [],

    initComponent: function () {
        Ext.applyIf(this, {
            enableDD: false,

            zoom: 4,
            lon: 0,
            lat: 0,

            basemap: null,      // The LLMAP.UI.MapLayer object of the current base layer
            maps: [],           // map objects for any additional maps being displayed

            use_zoom_bar: true
        });

        this.addEvents('addmap', 'olclick', 'olrightclick', 'mapmove', 'mapready', 'mapstatechange',  'removemap', 'setbasemap', 'srschange');

        LLMAP.UI.MapPanel.superclass.initComponent.call(this);
    },

    afterRender: function () {
        LLMAP.UI.MapPanel.superclass.afterRender.apply(this, arguments);
        
        if (!this.ownerCt) {
            this.renderMap();
        }
        else {
            this.ownerCt.addListener({
                scope: this,
                single: true,
                afterlayout: this.renderMap
            });
        
            this.ownerCt.addListener({
                scope: this,
                move: this.updateMapSize
            });
        }
    },

    renderMap: function () {
        var map = new OpenLayers.Map(this.body.dom, {
            controls: [],
            displayProjection: new OpenLayers.Projection('EPSG:4326'),
            units: 'degrees'
        });

        this.body.setStyle('background-color', '#94B9D6');

        this.loading = new LLMAP.UI.LoadingIndicator(this.body);

        var nav    = new OpenLayers.Control.Navigation({ handleRightClicks: true });
        var slider;

        if (this.use_zoom_bar) {
            slider = new OpenLayers.Control.PanZoomBar();
        }
        else {
            slider = new OpenLayers.Control.PanZoom();
        }

        map.addControl(slider);
        map.addControl(nav);
        map.addControl(new OpenLayers.Control.ZoomBox());

/*
        map.addControl(new OpenLayers.Control.OverviewMap({
            layers: [
                new OpenLayers.Layer.WMS(
                    'Overview Map',
                    'http://barbara.linguistlist.org/geoserver/wms',
                    {
                        layers: [ 'llmap:CONTINENTS' ],
                        styles: 'countries',
                        bgcolor: '#3C64FF'
                    },
                    {
                        singleTile: true,
                        ratio: 1
                    }
                )
            ]
        }));
*/
        nav.handlers.click.callbacks.click      = this.handleClick.createDelegate(this);
        nav.handlers.click.callbacks.rightclick = this.handleRightClick.createDelegate(this);

        if (this.controls.length) {
            map.addControls(this.controls);
        }

        map.events.register('moveend', this, function (ev) {
            this.fireEvent('mapmove', this);
        });

        this.olmap  = map;
        this.slider = slider;

        if (this.basemap) {
            this.basemaps = [ this.basemap ];

            delete this.basemap;
        }

        this.setBaseMap(this.basemaps[0]);

        if (this.overlay) {
            this.doAddMap(this.overlay);
        }

        this.setControlOffsets(0, 0, 0, 0);

        if (this.enableDD) {
            var dt_config = {
                ddGroup: 'MapDD',

                getTargetFromEvent: function (e) { return ; },

                notifyDrop: this.notifyDrop.createDelegate(this), 

                notifyEnter: function (source, e, data) {
                    if (e.getTarget('.olMapViewport') && source.dragData.node.attributes.map_id) {
                        if (this.overClass) {
                            this.el.addClass(this.overClass);
                        }

                        return this.dropAllowed;
                    }
                    else {
                        return this.dropNotAllowed;
                    }
                },

                notifyOver: function (source, e, data) {
                    if (e.getTarget('.olMapViewport') && source.dragData.node.attributes.map_id) {
                        return this.dropAllowed;
                    }
                    else {
                        return this.dropNotAllowed;
                    }
                }
            };

            if (this.dropConfig) {
                Ext.apply(dt_config, this.dropConfig);
            }

            this.dt = new Ext.dd.DropTarget(this.olmap.viewPortDiv, dt_config);
        }

        this.setDefaultCenter();

        for (var i = 0 ; i < this.maps.length ; i++) {
            this.doAddMap(this.maps[i]);
        }

        this.fireEvent('mapready', this);
    },

    addControl: function (control) {
        if (this.olmap) {
            this.olmap.addControl(control);
        }
        else {
            this.controls.push(control);
        }
    },

    addMap: function (map) {
        if (map.isSupportedProjection(this.srs)) {
            this.maps.push(map);

            if (this.olmap) {
                this.doAddMap(map);
            }
        }
        else {
            Ext.MessageBox.show({
                title: 'Incompatible Map',
                msg: 'The map "' + map.name + '" cannot be added, because it does not support the current map projection.',
                buttons: Ext.MessageBox.OK,
                icon: Ext.MessageBox.ERROR,
                width: 400
            });

            return;
        }
    },

    addPopup: function (popup) {
        this.olmap.addPopup(popup);
    },

    clearMap: function () {
        var i = this.maps.length;

        while (--i >= 0) {
            this.doRemoveMap(this.maps[i]);

            this.maps.splice(i, 1);
        }

        if (this.overlay) {
            for (i = 0 ; i < this.overlay.layers.length ; i++) {
                this.overlay.layers[i].visible = false;
            }

            this.overlay.updateLayers();
        }
    },

    doAddMap: function (map) {
        map.updateLayers();

        this.olmap.addLayer(map.map_layer);

        // Move the overlay back to the top
        if (this.overlay) {
            this.olmap.setLayerIndex(this.overlay.map_layer, this.olmap.getNumLayers() - 1);
        }

        this.fireEvent('addmap', this, map);
    },

    doRemoveMap: function (map) {
        this.olmap.removeLayer(map.map_layer);

        this.fireEvent('removemap', this, map);
    },

    getExtent: function () {
        var bounds = this.olmap.getExtent().toArray();

        return { minx: bounds[0], miny: bounds[1], maxx: bounds[2], maxy: bounds[3] };
    },

    getMapConfig: function () {
        var extent = this.olmap.getExtent();
        var config = {
            basemap: this.basemap.id,
            overlay: null,
            srs: this.olmap.getProjection(),
            width: this.olmap.size.w,
            height: this.olmap.size.h,
            minx: extent.left,
            miny: extent.bottom,
            maxx: extent.right,
            maxy: extent.top,
            maps: []
        };

        if (this.overlay !== undefined) {
            config.overlay = {
                id: this.overlay.id,
                layers: this.overlay.getVisibleLayers()
            };
        }

        for (var i = 0 ; i < this.maps.length ; i++) {
            var map = this.maps[i];

            if (map.ref) {
                config.maps.push({
                    ref: map.ref,
                    opacity: map.map_layer.opacity,
                    layers: map.getVisibleLayers()
                });
            }
        }

        return config;
    },

    getMaps: function () {
        return this.maps;
    },

    getDisplayProjection: function () {
        return this.olmap.displayProjection;
    },

    getNumMaps: function () {
        return this.maps.length;
    },

    getProjection: function () {
        return this.olmap.getProjection();
    },

    getProjectionObject: function () {
        return this.olmap.getProjectionObject();
    },

    handleClick: function (evt) {
        this.fireEvent('olclick', evt);
    },

    handleRightClick: function (evt) {
        this.fireEvent('olrightclick', evt);
    },

    loadMap: function (config, callback, scope) {
        var transid;

        /* this.loading may not exist here, like when loading initial maps in the viewer */

        if (this.loading) {
            this.loading.show();
        }

        if (typeof config == 'object') {
            transid = Ext.Ajax.request({
                method: 'GET',
                url: '/proxy/service-info.json',
                params: config,
                client_scope: scope,
                client_callback: callback,
                scope: this,
                failure: function () {
                    if (this.loading) {
                        this.loading.hide();
                    }
                },
                success: function (response, options) {
                    if (this.loading) {
                        this.loading.hide();
                    }

                    this.onLoadMap(response, options);
                }
            });
        }
        else {
            transid = Ext.Ajax.request({
                method: 'GET',
                url: config,
                client_scope: scope,
                client_callback: callback,
                scope: this,
                failure: function () {
                    //Ext.MessageBox.hide();
                    this.loading.hide();
                },
                success: function (response, options) {
                    //Ext.MessageBox.hide();
                    this.loading.hide();

                    this.onLoadMap(response, options);
                }
            });
        }
    },

    notifyDrop: function (source, e, data) {
        var map_id = source.dragData.node.attributes.map_id;

        if (e.getTarget('.olMapViewport') && map_id) {
            this.loadMap('/maps/' + map_id + '/map_layer.json', this.addMap, this);
        }
    },

    onLoadMap: function (response, options) {
        var result = Ext.decode(response.responseText);

        if (result.success) {
            var data = result.data;
            var type = data.type;
            var map;

            if (type == 'std') {
                map = new LLMAP.UI.StdMapLayer(data);
            }
            else {
                map = new LLMAP.UI.WMSMapLayer(data);
            }

            options.client_callback.call(options.client_scope, map);
        }
        else {
            Ext.MessageBox.show({
                title: 'Error',
                msg: 'The map could not be loaded. ' + result.message,
                buttons: Ext.MessageBox.OK,
                icon: Ext.MessageBox.ERROR,
                width: 400
            });
        }
    },

    onResize: function() {
        LLMAP.UI.MapPanel.superclass.onResize.apply(this, arguments);

        this.updateMapSize();
    },

    removeMap: function (map) {
        for (var i = 0 ; i < this.maps.length ; i++) {
            if (this.maps[i] === map) {
                this.maps.splice(i, 1);
                this.doRemoveMap(map);

                break;
            }
        }
    },

    removePopup: function (popup) {
        this.olmap.removePopup(popup);
    },

    setBaseMap: function (map) {
        if (this.basemap) {
            this.olmap.removeLayer(this.basemap.base_layer);
        }

        this.basemap = map;
        this.basemap.setProjection(this.srs);

        this.olmap.addLayer(this.basemap.base_layer);
        this.olmap.setBaseLayer(this.basemap.base_layer);

        if (this.overlay) {
            this.overlay.changeBaseMap(this.basemap);

            this.overlay.updateLayers();
        }

        if (!this.olmap.center) {
            this.setDefaultCenter();
        }

        this.fireEvent('setbasemap', this, this.basemap);
    },

    setProjection: function (srs) {
        var disabled = [];

        for (var i = 0 ; i < this.maps.length ; i++) {
            var map     = this.maps[i];
            var enabled = map.isSupportedProjection(srs);

            if (map.enabled !== enabled) {
                map.setEnabled(enabled);

                map.updateLayers();

                this.fireEvent('mapstatechange', this, map, enabled);
            }

            if (!enabled) {
                disabled.push(map.name);
            }
        }

        if (disabled.length) {
            var msg;

            if (disabled.length == 1) {
                msg = '<p>The map "' + disabled[0] + '" is not compatible with the new projection you have chosen. It has been disabled until you switch to a compatible projection.</p>';
            }
            else {
                msg = '<p>Some of the maps you are currently viewing are not compatible with the new projection you have chosen. The following maps have been disabled until you switch to a compatible projection:</p><ul style="list-style: disc outside; padding-left: 15px">';

                for (var j = 0 ; j < disabled.length ; j++) {
                    msg = msg + '<li>' + disabled[j] + '</li>';
                }

                msg = msg + '</ul>';
            }

            Ext.MessageBox.show({
                title: 'Warning',
                icon: Ext.MessageBox.WARNING,
                msg: msg,
                buttons: Ext.MessageBox.OK,
                closable: false,
                width: 450
            });
        }

        this.olmap.removeLayer(this.basemap.base_layer);

        this.srs = srs;

        this.basemap.setProjection(srs);

        this.olmap.center = null; // prevent hangs trying to reproject center; we'll reset bounds later
        this.olmap.addLayer(this.basemap.base_layer);
        this.olmap.setBaseLayer(this.basemap.base_layer);
        this.olmap.zoomToExtent(this.basemap.base_layer.maxExtent);

        this.fireEvent('srschange', this, srs);
    },

    setControlOffsets: function (off_left, off_right, off_top, off_bottom) {
        if (this.slider) {
            this.slider.moveTo(new OpenLayers.Pixel(off_left, off_top));
        }
    },

    setDefaultCenter: function () {
        var pos = new OpenLayers.LonLat(this.lon, this.lat);
        //pos.transform(this.olmap.displayProjection, this.olmap.getProjectionObject());

        this.olmap.setCenter(pos, this.zoom, false, true);
    },

    updateMaps: function () {
        for (var i = 0 ; i < this.maps.length ; i++) {
            this.maps[i].updateLayers();
        }
    },

    updateMapSize: function () {
        if (this.olmap) {
            this.olmap.updateSize();
        }
    },

    zoomToBounds: function (minlon, minlat, maxlon, maxlat) {
        var p1 = new OpenLayers.LonLat(minlon, minlat);
        var p2 = new OpenLayers.LonLat(maxlon, maxlat);

        p1.transform(this.olmap.displayProjection, this.olmap.getProjectionObject());
        p2.transform(this.olmap.displayProjection, this.olmap.getProjectionObject());

        this.olmap.zoomToExtent(new OpenLayers.Bounds(p1.lon, p1.lat, p2.lon, p2.lat));
    }
});

LLMAP.UI.BaseMapControl = Ext.extend(Ext.BoxComponent, {
    basemaps: [],

    selectedIndex: 0,

    map: null,

    initComponent: function () {
        LLMAP.UI.BaseMapControl.superclass.initComponent.apply(this, arguments);

        this.addEvents('change');
    },

    onRender: function (container, position) {
        LLMAP.UI.BaseMapControl.superclass.onRender.apply(this, arguments);

        var maps = [];

        var dom = {
            tag: 'div',
            cls: 'llmap-bmc',
            children: [
                {
                    tag: 'table',
                    children: [
                        {
                            tag: 'tr',
                            children: maps
                        }
                    ]
                }
            ]
        };

        for (var i = 0 ; i < this.map.basemaps.length ; i++) {
            var bm = this.map.basemaps[i];

            if (i == this.selectedIndex) {
                maps.push({ tag: 'td', cls: 'llmap-bmc-selected', html: bm.base_layer.name });
            }
            else {
                maps.push({ tag: 'td', html: bm.base_layer.name });
            }
        }

        if (position) {
            this.el = Ext.DomHelper.insertBefore(position, dom, true);
        } else {
            this.el = Ext.DomHelper.append(container, dom, true);
        }

        if (this.id) {
            this.el.dom.id = this.id;
        }

        this.cells = this.el.select('td');

        this.cells.addClassOnOver('llmap-bmc-hover');

        this.cells.on('click', function (e) {
            var index = e.target.cellIndex;

            if (index != this.selectedIndex) {
                this.cells.removeClass('llmap-bmc-selected');

                Ext.get(e.target).addClass('llmap-bmc-selected');

                this.selectedIndex = index;

                this.map.setBaseMap(this.map.basemaps[index]);
                //this.fireEvent('change', this, this.map.basemaps[index]);
            }
        }, this);
    }
});
/*
 * LLMAP.UI.TransparencyControl
 * Written by Joshua M. Thompson <joshua@linguistlist.org>
 *
 * This object class implements a slider for adjusting a map's opacity value
 */

/*jslint browser: true, devel: true, eqeqeq: false, onevar: false, plusplus: false, white: false */
/*global Ext: true, LLMAP: true, OpenLayers: true, window: true */

LLMAP.UI.TransparencyControl = Ext.extend(Ext.Window, {
    map: null,

    initComponent: function () {
        var initialValue = 100 - (this.map.map_layer.opacity * 100);

        Ext.apply(this, {
            layout: 'border',
            title: 'Change Transparency',
            height: 150,
            width: 375,
            closable: false,
            buttons: [
                {
                    text: 'Close',
                    scope: this,
                    handler: function () { this.close(); }
                }
            ],
            items: [
                {
                    region: 'center',
                    bodyStyle: 'padding: 10px',
                    items: {
                        xtype: 'slider',
                        anchor: '-10',
                        increment: 1,
                        minValue: 0,
                        maxValue: 100,
                        value: initialValue,
                        vertical: false,
                        listeners: {
                            scope: this,
                            change: function (slider, value, thumb) {
                                this.statusPanel.body.update(this.formatValue(value));

                                this.map.map_layer.setOpacity((100 - value) / 100);
                            }
                        }
                    }
                },
                {
                    region: 'south',
                    ref: 'statusPanel',
                    bodyStyle: 'padding: 5px; font-size: 14px; font-weight: bold; height: 20px; text-align: center;',
                    html: this.formatValue(initialValue)
                }
            ]
        });

        LLMAP.UI.TransparencyControl.superclass.initComponent.call(this);
    },

    formatValue: function (value) {
        return value + '%';
    }
});

/*
 * LLMAP.UI.TemporalControl
 * Written by Joshua M. Thompson <joshua@linguistlist.org>
 *
 * This object class implements a slider for adjusting the year of a temporal map
 */

LLMAP.UI.TemporalControl = Ext.extend(Ext.Window, {
    map: null,

    initComponent: function () {
        Ext.apply(this, {
            layout: 'border',
            title: 'Change Year',
            height: 150,
            width: 375,
            closable: false,
            buttons: [
                {
                    text: 'Close',
                    scope: this,
                    handler: function () { this.close(); }
                }
            ],
            items: [
                {
                    region: 'center',
                    bodyStyle: 'padding: 10px',
                    items: {
                        xtype: 'slider',
                        anchor: '-10',
                        increment: this.map.year_increment,
                        minValue: this.map.start_year,
                        maxValue: this.map.end_year,
                        value: this.map.current_year,
                        vertical: false,
                        listeners: {
                            scope: this,
                            change: function (slider, value, thumb) {
                                this.statusPanel.body.update(this.formatYear(value));
                            },
                            changecomplete: function (slider, value, thumb) {
                                this.map.setYear(value);
                            }
                        }
                    }
                },
                {
                    region: 'south',
                    ref: 'statusPanel',
                    bodyStyle: 'padding: 5px; font-size: 14px; font-weight: bold; height: 20px; text-align: center;',
                    html: this.formatYear(this.map.current_year)
                }
            ]
        });

        LLMAP.UI.TemporalControl.superclass.initComponent.call(this);
    },

    formatYear: function (year) {
        var suffix = (year < 0)? 'BC' : 'AD';

        return Math.abs(year) + ' ' + suffix;
    }
});

LLMAP.UI.MapPanel.Control = Ext.extend(Ext.BoxComponent, {
    map: null,
    icon: null,
    tooltip: null,

    onRender: function (container, position) {
        var fragment = {
            tag: 'img',
            cls: 'map-panel-control',
            src: this.icon
        };

        if (position) {
            this.el = Ext.DomHelper.insertBefore(position, fragment, true);
        } else {
            this.el = Ext.DomHelper.append(container, fragment, true);
        }

        if (this.id) {
            this.el.dom.id = this.id;
        }

        this.el.on('click', function (e, t, o) { this.onClick(e, t, o); }, this);

        if (this.tooltip) {
            Ext.QuickTips.register({target: this.el, text: this.tooltip });
        }
    },

    onClick: function (e, t, o) {}
});

LLMAP.UI.MapPanel.Projection = Ext.extend(LLMAP.UI.MapPanel.Control, {
    icon: '/icons/geosilk/ruler.png',
    tooltip: 'Change the display projection of the map',

    onClick: function (e, t, o) {
        var srs = parseInt(this.map.getProjection().replace(/^EPSG:/, ''));

        var w = new LLMAP.UI.ProjectionChooser({
            value: srs,
            listeners: {
                scope: this,
                select: function (chooser, newValue) {
                    this.map.setProjection('EPSG:' + newValue);
                }
            }
        });

        w.show();
    }
});

LLMAP.UI.MapPanel.SetYear = Ext.extend(LLMAP.UI.MapPanel.Control, {
    icon: '/icons/silk/clock.png',
    tooltip: 'Change map display to a different year',

    onClick: function (e, t, o) {}
});

/*
 * LLMAP.UI.MapPanel.Options
 * Written by Joshua M. Thompson <joshua@linguistlist.org>
 *
 * This class implements a panel with controls pertaining to the entire
 * map panel. This includes the Set Projection, Set Time Period,
 * Email Map and Print map controls.
 */

LLMAP.UI.MapPanel.Options = Ext.extend(Ext.BoxComponent, {
    title: null,

    controls: [],

    rendered: false,

    initComponent: function () {
        LLMAP.UI.MapPanel.Options.superclass.initComponent.call(this);
    },

    addControl: function (c) {
        this.controls.push(c);

        if (this.rendered) {
            c.render(this.inner);
        }
    },

    onRender: function (container, position) {
        var fragment = {
            tag: 'div',
            cls: 'map-options-panel'
        };

        if (position) {
            this.el = Ext.DomHelper.insertBefore(position, fragment, true);
        } else {
            this.el = Ext.DomHelper.append(container, fragment, true);
        }

        if (this.id) {
            this.el.dom.id = this.id;
        }

        if (this.title) {
            Ext.DomHelper.append(this.el, { tag: 'h1', html: this.title });
        }

        this.inner = Ext.DomHelper.append(this.el, {
            tag: 'div',
            cls: 'map-options-inner'
        }, true);

        this.rendered = true;

        for (var i = 0 ; i < this.controls.length ; i++) {
            var c = this.controls[i];

            c.render(this.inner);
        }
    }
});

LLMAP.UI.ProjectionChooser = Ext.extend(Ext.Window, {
    value: 4326,

    initComponent: function () {
        Ext.apply(this, {
            layout: 'fit',
            title: 'Change Map Projection',
            height: 324,
            width: 600,
            closable: false,
            autoScroll: true,
            buttons: [
                {
                    iconCls: 'icon-projection',
                    text: 'Set Projection',
                    scope: this,
                    handler: this.setProjection
                },
                {
                    iconCls: 'icon-cancel',
                    text: 'Close',
                    scope: this,
                    handler: function () { this.close(); }
                }
            ],
            items: {
                xtype: 'dataview',
                ref: 'dataView',
                border: false,
                itemSelector: 'div.proj-info',
                overClass: 'proj-info-over',
                selectedClass: 'proj-info-selected',
                singleSelect: true,
                store: LLMAP.SRS.zfCreateStore({
                    proxyOptions: {
                        url: '/srs/preferred.json',
                    },
                    autoLoad: true,
                    writeable: false
                }),
                tpl: new Ext.XTemplate(
                    '<tpl for=".">',
                        '<div class="proj-info" id="epsg{srid}">',
                            '<div class="proj-info-preview">',
                                '<img alt="Preview" src="/images/proj/{srid}-small.png" />',
                            '</div>',
                            '<div class="proj-info-description">',
                                '<h1>{label}</h1>',
                                '{description}',
                            '</div>',
                        '</div>',
                        '<div class="x-clear"></div>',
                    '</tpl>'
                ),
                listeners: {
                    scope: this,
                    dblclick: this.setProjection
                }
            }
        });

        LLMAP.UI.ProjectionChooser.superclass.initComponent.call(this);

        this.addEvents('select');

        this.dataView.getStore().on('load', function () {
            if (this.value) {
                this.dataView.select('epsg' + this.value);
            }
        }, this);
    },

    setProjection: function () {
        var sel = this.dataView.getSelectedRecords();

        this.fireEvent('select', this, sel[0].get('srid'));

        this.close();
    }
});

/*
 * LLMAP.Preferences
 * Written by Joshua M. Thompson <joshua@linguistlist.org>
 *
 * A singleton class for client UI preferences management via cookies
 */

/*jslint browser: true, devel: true, eqeqeq: false, onevar: false, plusplus: false, white: false */
/*global Ext: true, LLMAP: true, OpenLayers: true */

LLMAP.Preferences = {
    cookieName: 'llmap_mapui',
    prefs: {},

    init: function () {
    },

    create: function (config) {
        var type = config.xtype;
        var pref;

        if (type == 'yesno') {
            pref = new LLMAP.YesNoPref(config);
        }
        else if (type == 'showtip') {
            pref = new LLMAP.ShowTipPref(config);
        }
        else {
            return null; // FIXME: probably error out here, or something.
        }

        this.prefs[pref.name] = pref;

        return pref;
    },

    get: function (name, callback, scope, data) {
        return this.prefs[name].get(this, callback, scope, data);
    },

    load: function () {
        var date     = new Date();
        var provider = new Ext.state.CookieProvider({
            expires: new Date(date.getTime() + (1000 * 60 * 60 * 24 * 365 * 10)) // 10 years, in milliseconds
        });
        var data     = provider.get(this.cookieName, {});

        for (var name in data) {
            var pref = this.prefs[name];

            if (pref) {
                pref.value = data[name];
            }
        }

        this.provider = provider;
    },

    save: function () {
        var data = {};

        for (var name in this.prefs) {
            data[name] = this.prefs[name].value;
        }

        this.provider.set(this.cookieName, data);
    }
};

LLMAP.Pref = function (config) {
    Ext.applyIf(config, {
        name: 'preference',
        title: 'Preference',
        text: 'What is the airspeed velocity of an unladen swallow?',
        value: null
    });

    Ext.apply(this, config);
};

LLMAP.ShowTipPref = Ext.extend(LLMAP.Pref, {
    title: 'LL-MAP Tip',
    get: function (manager, callback, scope, data) {
        if (this.value === null) {
            Ext.Msg.show({
                title: this.title,
                msg: this.text,
                icon: Ext.MessageBox.INFO,
                buttons: {
                    ok: true,
                    cancel: 'Don\'t show this again'
                },
                scope: this,
                fn: function (btn) {
                    if (btn != 'ok') {
                        this.value = true;

                        manager.save();
                    }

                    callback.call(scope, this, this.value, data);
                }
            });
        }
        else {
            // preference is already set

            callback.call(scope, this, this.value, data);
        }
    }
});

LLMAP.YesNoPref = Ext.extend(LLMAP.Pref, {
    get: function (manager, callback, scope, data) {
        if (this.value === null) {
            Ext.Msg.show({
                title: this.title,
                msg: this.text,
                icon: Ext.MessageBox.QUESTION,
                buttons: {
                    yes: true,
                    no: true,
                    cancel: 'Don\'t ask again'
                },
                scope: this,
                fn: function (btn) {
                    var value;

                    if (btn == 'yes') {
                        value = true;
                    }
                    else if (btn == 'no') {
                        value = false;
                    }
                    else {
                        this.value = value = false;

                        manager.save();
                    }

                    callback.call(scope, this, value, data);
                }
            });
        }
        else {
            // preference is already set

            callback.call(scope, this, this.value, data);
        }
    }
});
/*
 * Social Networking controls
 * Written by Joshua M. THompson <joshua@linguistlist.org>
 *
 * This file contains map controls for map sharing via email
 * and social networks.
 */

/*jslint browser: true, devel: true, eqeqeq: false, onevar: false, plusplus: false, white: false */
/*global Ext: true, LLMAP: true, OpenLayers: true, window: true, FB: true, gapi: true */

Ext.namespace('LLMAP.UI.Social');

LLMAP.UI.Social.Control = Ext.extend(Ext.BoxComponent, {
    href: null,

    icon: null,
    tooltip: null,

    onRender: function (container, position) {
        var fragment = {
            tag: 'img',
            cls: 'social-control',
            src: this.icon
        };

        if (position) {
            this.el = Ext.DomHelper.insertBefore(position, fragment, true);
        } else {
            this.el = Ext.DomHelper.append(container, fragment, true);
        }

        if (this.id) {
            this.el.dom.id = this.id;
        }

        this.el.on('click', function (e, t, o) { this.onClick(e, t, o); }, this);

        if (this.tooltip) {
            Ext.QuickTips.register({ target: this.el, text: this.tooltip });
        }
    },

    onClick: function (e, t, o) {}
});

/*
 * LLMAP.UI.Social.FBShare
 * Written by Joshua M. Thompson <joshua@linguistlist.org>
 *
 * Social control for implementing a Share to Facebook button
 */

LLMAP.UI.Social.FBShare = Ext.extend(LLMAP.UI.Social.Control, {
    icon: '/icons/social/facebook.png',
    tooltip: 'Share this map via Facebook',

    onClick: function (e, t, o) {
        var url;

        if (typeof this.href == 'function') {
            url = this.href.call(this);
        }
        else {
            url = this.href;
        }

        var w = window.open(
            'https://www.facebook.com/sharer.php?u=' + encodeURIComponent(url),
            'fbshare',
            'status=0,toolbar=0,location=0,height=400,width=600'
        );
    }
});

/*
 * LLMAP.UI.Social.FBLike
 * Written by Joshua M. Thompson <joshua@linguistlist.org>
 *
 * Social control for FaceBook "Like" button
 */

LLMAP.UI.Social.FBLike = Ext.extend(LLMAP.UI.Social.Control, {
    font: 'arial',
    layout: 'button_count',
    send: 'false',
    show_faces: 'false',
    height: 21,
    width: 90,

    onRender: function (container, position) {
        var fragment = {
            tag: 'div',
            cls: 'social-control'
        };

        if (position) {
            this.el = Ext.DomHelper.insertBefore(position, fragment, true);
        } else {
            this.el = Ext.DomHelper.append(container, fragment, true);
        }

        if (this.id) {
            this.el.dom.id = this.id;
        }

        Ext.DomHelper.append(this.el, '<iframe src="//www.facebook.com/plugins/like.php?href=' + encodeURIComponent(this.href) + '&send=' + this.send + '&layout=' + this.layout + '&width=' + this.width + '&show_faces=' + this.show_faces + '&font=' + this.font + '" scrolling="no" frameborder="0" style="border:none; width:' + this.width + 'px; height:' + this.height + 'px"></iframe>');
    }
});

/*
 * LLMAP.UI.Social.GooglePlus
 * Written by Joshua M. Thompson <joshua@linguistlist.org>
 *
 * Social control for Google Plus
 */

LLMAP.UI.Social.GooglePlus = Ext.extend(LLMAP.UI.Social.Control, {
    size: 'small',
    annotation: 'none',

    onRender: function (container, position) {
        var fragment = {
            tag: 'div',
            cls: 'social-control'
        };

        if (position) {
            this.el = Ext.DomHelper.insertBefore(position, fragment, true);
        } else {
            this.el = Ext.DomHelper.append(container, fragment, true);
        }

        if (this.id) {
            this.el.dom.id = this.id;
        }

        this.plusone = Ext.DomHelper.append(this.el, '<div></div>', true);

        gapi.plusone.render(this.plusone.id, {
            href: this.href,
            size: this.size,
            annotation: this.annotation
        });
    }
});

/*
 * LLMAP.UI.Social.Twitter
 * Written by Joshua M. Thompson <joshua@linguistlist.org>
 *
 * Social control for Tweet button
 */

LLMAP.UI.Social.Twitter = Ext.extend(LLMAP.UI.Social.Control, {
    count: 'horizontal',
    text: null,
    url: null,
    via: null,

    height: 21,
    width: 55,

    onRender: function (container, position) {
        var fragment = {
            tag: 'div',
            cls: 'social-control'
        };

        if (position) {
            this.el = Ext.DomHelper.insertBefore(position, fragment, true);
        } else {
            this.el = Ext.DomHelper.append(container, fragment, true);
        }

        if (this.id) {
            this.el.dom.id = this.id;
        }

        var url = '//platform.twitter.com/widgets/tweet_button.html?count=' + this.count;

        if (this.url) {
            url = url + '&url=' + encodeURIComponent(this.url);
        }

        if (this.text) {
            url = url + '&text=' +encodeURIComponent(this.text);
        }

        if (this.via) {
            url = url + '&via=' +encodeURIComponent(this.via);
        }

        Ext.DomHelper.append(this.el, '<iframe allowtransparency="true" frameborder="0" scrolling="no" src="' + url + '" style="width:' + this.width + 'px; height:' + this.height + 'px;"></iframe>');
    }
});

/*
 * LLMAP.UI.Social.Email
 * Written by Joshua M. Thompson <joshua@linguistlist.org>
 *
 * Social control for sharing a page URL via email
 */

LLMAP.UI.Social.Email = Ext.extend(LLMAP.UI.Social.Control, {
    icon: '/icons/social/email.png',
    tooltip: 'Email this map to a friend',

    action: null,
    options: null,

    onClick: function (e, t, o) {
        var options = this.options;

        if (typeof this.options == 'function') {
            options = this.options.call();
        }
        else {
            options = this.options;
        }

        var w = new LLMAP.UI.EmailForm({ action: this.action, options: options });
        w.show();
    }
});

/*
 * LLMAP.UI.Social.Cite
 * Written by Joshua M. Thompson <joshua@linguistlist.org>
 *
 * Social control for generating a citation to a map
 */

LLMAP.UI.Social.Cite = Ext.extend(LLMAP.UI.Social.Control, {
    icon: '/icons/social/cite.png',
    tooltip: 'Get a citation for this map',

    citation: '',

    onClick: function (e, t, o) {
        Ext.MessageBox.show({
            title: 'Citation',
            icon: Ext.MessageBox.INFO,
            msg: 'Copy and paste the citation below to cite this map in a publication:',
            prompt: true,
            value: this.citation,
            buttons: Ext.MessageBox.OK,
            width: 600
        });
    }
});

/*
 * LLMAP.UI.Social.PermaLink
 * Written by Joshua M. Thompson <joshua@linguistlist.org>
 *
 * Social control for providing a user with a permanent link to a map
 */

LLMAP.UI.Social.PermaLink = Ext.extend(LLMAP.UI.Social.Control, {
    icon: '/icons/social/permalink.png',
    tooltip: 'Get a permanent link to this map',

    onClick: function (e, t, o) {
        var url;

        if (typeof this.href == 'function') {
            url = this.href.call();
        }
        else {
            url = this.href;
        }

        Ext.MessageBox.show({
            title: 'Permalink',
            icon: Ext.MessageBox.INFO,
            msg: 'Copy and paste the link below to share this map:',
            prompt: true,
            value: url,
            buttons: Ext.MessageBox.OK,
            width: 600
        });
    }
});

/*
 * LLMAP.UI.EmailForm
 * Written by Joshua M. Thompson <joshua@linguistlist.org>
 *
 * This class provides an Ext.Panel for allowing the user
 * to share an LL-MAP URL via email.
 */

LLMAP.UI.EmailForm = Ext.extend(Ext.Window, {
    layout: 'fit',
    title: 'Share via Email',
    closable: false,
    hidden: false,
    modal: true,
    width: 475,
    height: 425,

    action: null,

    options: {},

    initComponent: function () {
        Ext.apply(this, {
            items: [
                {
                    xtype: 'form',
                    border: false,
                    bodyStyle: 'padding: 10px',
                    labelAlign: 'right',
                    labelWidth: 100,
                    labelPadding: 5,
                    monitorValid: true,
                    baseParams: this.options,
                    items: [
                        {
                            xtype: 'textfield',
                            name: 'from_name',
                            fieldLabel: 'Your Name',
                            width: 320,
                            allowBlank: false,
                            emptyText: 'Please enter your name',
                            blankText: 'You must provide your name',
                            value: LLMAP.account === null? '' : LLMAP.account.userfn + ' ' + LLMAP.account.userln
                        },
                        {
                            xtype: 'textfield',
                            name: 'from_email',
                            fieldLabel: 'Your Email',
                            width: 320,
                            allowBlank: false,
                            emptyText: 'Please enter your email address',
                            blankText: 'You must provide your email address',
                            value: LLMAP.account === null? '' : LLMAP.account.email,
                            vtype: 'email'
                        },
                        {
                            xtype: 'textfield',
                            name: 'to_email',
                            fieldLabel: 'Friend\'s Email',
                            width: 320,
                            allowBlank: false,
                            emptyText: 'Please enter the destination email address',
                            blankText: 'You must provide a destination email address',
                            vtype: 'email'
                        },
                        {
                            xtype: 'textarea',
                            name: 'note',
                            fieldLabel: 'Note',
                            height: 125,
                            width: 320,
                            allowBlank: true,
                            emptyText: 'Enter an optional note to add to this message'
                        },
                        {
                            xtype: 'recaptcha',
                            name: 'recaptcha',
                            fieldLabel: 'Verification',
                            id: 'recaptcha',
                            publickey: '6LeTzrkSAAAAAATl1zNpgcXT6MwRHp9Ri5gnJNme',
                            lang: 'en',
                            theme: 'white'
                        }
                    ],
                    listeners: {
                        scope: this,
                        clientvalidation: function (fp, valid) {
                            this.buttons[0].setDisabled(!valid);
                        }
                    }
                }
            ],
            buttons: [
                {
                    text: 'Send Message',
                    scope: this,
                    handler: function () {
                        this.sendMessage();
                    }
                },
                {
                    text: 'Cancel',
                    scope: this,
                    handler: function () { this.close(); }
                }
            ]
        });

        LLMAP.UI.EmailForm.superclass.initComponent.call(this);
    },

    sendMessage: function () {
        this.getComponent(0).getForm().submit({
            url: this.action,
            clientValidation: true,
            submitEmptyText: false,
            waitMsg: 'Sending Message...',
            scope: this,
            success: function (form, action) {
                this.close();

                Ext.Msg.alert('Success', 'Your message has been sent.');
            },
            failure: function (form, action) {
                if (action.failureType === Ext.form.Action.CONNECT_FAILURE) {
                    Ext.Msg.alert('Error', 'Status:'+action.response.status+': '+ action.response.statusText);
                }
                else if (action.failureType === Ext.form.Action.SERVER_INVALID) {
                    if (action.result.errorMsg) {
                        Ext.Msg.alert('Error', action.result.errorMsg);
                    }
                }
            }
        });
    }
});
/*
 * LLMAP.UI.ProjectionSelector
 *
 * This is an ExtJS trigger field that provides a UI for selecting a map projection.
 * It functions in a similar manner to a ComboBox in that a hidden field is used to
 * store the actual SRID, with the text field used to display a descriptive string.
 * 
 */

/*jslint browser: true, devel: true, eqeqeq: false, onevar: false, plusplus: false, white: false */
/*global Ext: true, LLMAP: true, OpenLayers: true */

LLMAP.UI.ProjectionSelector = Ext.extend(Ext.form.TriggerField, {
    editable: false,

    submitValue: undefined,

    getValue : function(){
        return Ext.isDefined(this.value)? this.value : '';
    },

    initEvents: function () {}, // we don't want any of the stock events from Field/TextField

    onRender: function (ct, position) {
        if (this.hiddenName && !Ext.isDefined(this.submitValue)) {
            this.submitValue = false;
        }

        LLMAP.UI.ProjectionSelector.superclass.onRender.call(this, ct, position);

        if (this.hiddenName) {
            this.hiddenField = this.el.insertSibling({
                tag: 'input',
                type: 'hidden',
                name: this.hiddenName,
                id: (this.hiddenId || Ext.id())
            }, 'before', true);
        }

    },

    onTriggerClick: function () {
        var w = new LLMAP.UI.SRSBrowser({
            listeners: {
                scope: this,
                select: function (w, srs) {
                    this.setValue(srs.get('srid') + ';' + srs.get('label'));
                }
            }
        });

        w.show();
    },

    setValue : function (v) {
        var srid = v.split(';');
        var text;

        if (srid.length < 2) {
            text = 'EPSG:' + v;
        }
        else {
            v    = srid[0];
            text = srid[1];
        }

        if (this.hiddenField) {
            this.hiddenField.value = Ext.value(v, '');
        }

        LLMAP.UI.ProjectionSelector.superclass.setValue.call(this, text);

        this.value = v;

        return this;
    }
});

/*
 * LLMAP.UI.SRSGrid
 * Written by Joshua M. Thompson <joshua@linguistlist.org>
 *
 * This implements a GridPanel for displaying SRS entries
 */

LLMAP.UI.SRSGrid = Ext.extend(Ext.grid.GridPanel, {
    initComponent: function () {
        Ext.apply(this, {
            xtype: 'grid',
            colModel: new Ext.grid.ColumnModel([
                {
                    id: 'srid',
                    dataIndex: 'srid',
                    header: 'SRID',
                    width: 75,
                    sortable: true,
                    groupable: false
                },
                {
                    id: 'name',
                    dataIndex: 'name',
                    header: 'SRS Name',
                    width: 300,
                    sortable: true,
                    groupable: false,
                    resizable: false
                },
                {
                    id: 'preferred',
                    dataIndex: 'preferred',
                    header: 'Preferred',
                    width: 25,
                    hidden: true,
                    sortable: true,
                    groupable: true,
                    resizable: false,
                    groupRenderer: function (value) {
                        if (value) {
                            return 'Preferred';
                        }
                        else {
                            return 'Other';
                        }
                    }
                }
            ]),
            sm: new Ext.grid.RowSelectionModel({ singleSelect: true }),
            autoExpandColumn: 'name',
            view: new Ext.grid.GroupingView({
                groupTextTpl: '{group}'
            })
        });

        LLMAP.UI.SRSGrid.superclass.initComponent.call(this);
    }
});

/*
 * LLMAP.UI.SRSBrowser
 * Written by Joshua M. Thompson <joshua@linguistlist.org>
 *
 * This implements an Ext.Window for selecting one or more codes
 */

LLMAP.UI.SRSBrowser = Ext.extend(Ext.Window, {
    initComponent: function () {
        this.store = LLMAP.SRS.zfCreateStore({
            autoLoad: { params: { start: 0, limit: 10 } },
            grouping: true,
            groupField: 'preferred',
            remoteSort: true
        });

        Ext.apply(this, {
            title: 'Select SRS',
            layout: 'fit',
            modal: true,
            height: 352,
            width: 500,
            items: new LLMAP.UI.SRSGrid({
                ref: 'srs_grid',
                border: false,
                height: 250,
                width: 415,
                store: this.store,
                tbar: [],
                bbar: new Ext.PagingToolbar({
                    store: this.store,
                    pageSize: 10,
                    displayInfo: true,
                    displayMsg: 'Results {0} - {1} of {2}',
                    emptyMsg: 'No matches'
                }),
                plugins: [
                    new Ext.ux.grid.Search({
                        autoFocus: true,
                        checkIndexes: [ 'srid', 'name' ],
                        menuStyle: 'radio',
                        minChars: 2,
                        mode: 'remote',
                        position: 'top',
                        width: 400
                    })
                ],
                listeners: {
                    scope: this,
                    rowclick: function (grid, rowIndex, e) {
                        this.select_button.setDisabled(false);
                    },
                    rowdblclick: this.selectSRS
                }
            }),
            buttons: [
                {
                    text: 'Select',
                    ref: '../select_button',
                    iconCls: 'icon-arrow-right',
                    width: 125,
                    scope: this,
                    disabled: true,
                    handler: this.selectSRS
                },
                {
                    text: 'Cancel',
                    scope: this,
                    handler: function () { this.close(); }
                }
            ]
        });

        LLMAP.UI.SRSBrowser.superclass.initComponent.call(this);

        this.addEvents('select');
    },

    selectSRS: function () {
        var srs = this.srs_grid.getSelectionModel().getSelected();

        this.fireEvent('select', this, srs);

        this.close();
    }
});
/*
 * LLMAP.UI.UploadPanel
 * Written by Joshua M. Thompson <joshua@linguistlist.org>
 *
 * This class provides an Ext.Panel for allowing the user
 * to upload up to four files for subsequent processing. 
 */

/*jslint browser: true, devel: true, eqeqeq: false, onevar: false, plusplus: false, white: false */
/*global Ext: true, LLMAP: true, OpenLayers: true, window: true */

LLMAP.UI.UploadPanel = Ext.extend(Ext.FormPanel, {
    mode: 'data',

    initComponent: function () {
        Ext.apply(this, {
            bodyStyle: 'padding: 10px',
            fileUpload: true,
            labelWidth: 50,
            items: [
                {
                    xtype: 'fileuploadfield',
                    name: 'filename1',
                    fieldLabel: 'File #1',
                    allowBlank: true,
                    emptyText: 'Select a file...',
                    buttonText: '',
                    buttonCfg: {
                        iconCls: 'icon-folder-go'
                    },
                    anchor: '100%'
                },
                {
                    xtype: 'fileuploadfield',
                    name: 'filename2',
                    fieldLabel: 'File #2',
                    allowBlank: true,
                    emptyText: 'Select a file...',
                    buttonText: '',
                    buttonCfg: {
                        iconCls: 'icon-folder-go'
                    },
                    anchor: '100%'
                },
                {
                    xtype: 'fileuploadfield',
                    name: 'filename3',
                    fieldLabel: 'File #3',
                    allowBlank: true,
                    emptyText: 'Select a file...',
                    buttonText: '',
                    buttonCfg: {
                        iconCls: 'icon-folder-go'
                    },
                    anchor: '100%'
                },
                {
                    xtype: 'fileuploadfield',
                    name: 'filename4',
                    fieldLabel: 'File #4',
                    allowBlank: true,
                    emptyText: 'Select a file...',
                    buttonText: '',
                    buttonCfg: {
                        iconCls: 'icon-folder-go'
                    },
                    anchor: '100%'
                },
                {
                    xtype: 'displayfield',
                    name: 'message',
                    hideLabel: true,
                    cls: 'upload-window-error',
                    value: ''
                },
                {
                    xtype: 'hidden',
                    name: 'mode',
                    value: this.mode
                }
            ]
        });

        LLMAP.UI.UploadPanel.superclass.initComponent.call(this);

        this.addEvents('uploadcomplete', 'uploadfailed');
    },

    beginUpload: function () {
        this.getForm().submit({
            url: '/uploads.json',
            scope: this,
            waitMsg: 'Uploading files...',
            success: function (form, action) {
                this.fireEvent('uploadcomplete', this, action.result);
            },
            failure: function (form, action) {
                this.fireEvent('uploadfailed', this, action.result);
            }
        });
    }
});

