// Identify the Google base URL, the Google calendar base URL, and the XML
// feed beginning and end URL components that surround the calendar ID of a
// group.
//
var google_base_url = "http://www.google.com";
var calendar_base_url = google_base_url + "/calendar"
var xml_subscription_pre = calendar_base_url + "/feeds/";
var xml_subscription_post = "/public/basic";

// Interesting, Google only allows certain specific colors, as identified.
//
var allowed_google_colors = new Array("A32929", "B1365F", "7A367A", "5229A3",
    "29527A", "2952A3", "1B887A", "28754E", "0D7813", "528800", "88880E",
    "AB8B00", "BE6D00", "B1440E", "865A5A", "705770", "4E5D6C", "5A6986",
    "4A716C", "6E6E41", "8D6F47");

// Make an identifier that breaks up the Google calendar iframe src URL value
// into components: [1] before group-specific information, [2] group-specific
// information, and [3] after group-specific information. Also, set a variable
// to get the calendar ID from the group-specific information, the source.
// Finally, identify the the calendar database field name, which is used by
// both the calendar and the registry.
//
var calendar_source_matcher = /^(.*(?:&|embed\?))(src=.*&)(ctz=.*)$/i;
var calendar_id_from_source_matcher = /^src=(.*)&$/i;
var cal_db_field_name = "google_calendar_url";

// Define the pixel span for a cell in the color-picker table for calendar
// event colors. Also, define the opacity and the color of the range box on
// the map.
//
var color_cell_span = "16px";
var range_box_opacity = 0.92;
var range_box_color = "#ff0000";

// Define the default latitude and longitude of the map location if we don't
// have a valid location. Also make a default zoom level (scale) for the map.
// This stuff is seldom actually used.
//
var default_lat = 35.98777;
var default_lng = -95.136295;
var default_map_zoom = 11;

// Define the number of meters in a kilometer and a mile.
//
var meters_per_km = 1000.0;
var meters_per_mi = 1609.344;

// We use Google maps, so make variables to hold the map, its marker, and its
// API client. Make variables to store the input text version of the last
// location and to store the map range overlay.
//
var map, map_marker, map_api;
var last_location, range_overlay;

// This function returns a random, but official, Google color.
//
function taste_the_rainbow()
    {
    var colors = allowed_google_colors;
    return colors[Math.floor(Math.random() * colors.length)];
    }

// the allowable Google calendar event colors. The table is composed of small
// square cells for each color, in two rows. Each cell has an onclick event to
// callback the appropriate function to set the color of the group's calendar
// event color, passing into the function the text of the hexadecimal color
// code. In registry mode, the HTML for a span to display the color picker is
// also printed.
//
function print_color_picker_html(registry_mode)
    {

    // Define the color of the color pick'n table, as well as each cell's
    // dimensions.
    //
    var border_color_style = "border-color: black";
    var cell_dimensions = "width=\"" + color_cell_span + "\" height=\""
        + color_cell_span + "\"";

    // Determine the number of columns we need in order to fit all the colors
    // into a table of two rows. Then start the table HTML code and the first
    // row. These table specifications are awfully particular!
    //
    var columns = Math.ceil(allowed_google_colors.length / 2);
    var html = "<table border=\"1px\" style=\"border-collapse: collapse; "
        + border_color_style + ";\">\n";
    html += "    <tr>\n";

    // Define the function, as appropriate for our mode, of our callback
    // function when a color is chosen, passing in the hexadecimal color text.
    //
    var set_color_function = "set_calendar_group_event_color";
    if (registry_mode)
        {set_color_function = "set_registry_color_text";}
    
    // Create the HTML code for a cell for each Google color that is allowed,
    // on two rows. Each cell should have a onclick call to set the registry
    // color text to the color of that cell.
    //
    for (index in allowed_google_colors)
        {

        // If this is the junction between the first and second row, add the
        // HTML code to end the last row and start the new one.
        //
        if (Number(index) && index % columns == 0)
            {
            html += "    </tr>\n";
            html += "    <tr>\n";
            }

        // Start the table cell with an onclick specification to set the
        // color to that of this cell that we are building, and the cell's
        // dimensions.
        //
        html += "        <td onclick=\"" + set_color_function + "('"
            + allowed_google_colors[index] + "')\"\n";
        html += "            " + cell_dimensions + "\n";

        // Specify that color in the background of the cell and the cell's
        // border color too. Finish out the cell specification.
        //
        html += "            style=\"" + border_color_style
            + "; background-color: #" + allowed_google_colors[index]
            + ";\"></td>\n";
        }

    // Only allow the unsetting of the color outside of the group registry
    // mode, because we always want a default color to be chosen for a group's
    // calendar events. Otherwise, append on the last row an additional cell
    // for unsetting the color specification.
    //
    if ( ! registry_mode)
        {
        html += "        <td title=\"Click here to use the group&rsquo;s"
            + " default color (may change)\"\n"
            + " " + cell_dimensions + " onclick=\"" + set_color_function
            + "('')\" style=\"" + border_color_style + "\"></td>\n";
        }

    // Close out this last row and the table, and create and print out full
    // the HTML code for our color picker.
    //
    html += "    </tr>\n";
    html += "</table>";
    write_ib("color_ib", "Color Picker", html);

    // For our group registry, we might as well also print the HTML code for a
    // span to display the color picker we just made.
    //
    if (registry_mode)
        {
        document.write("<span onmouseover=\"up_ib('color_ib');\""
            + " onclick=\"stick_ib('color_ib')\""
            + " onmouseout=\"un_ib()\">[color picker]</span>\n");
        }
    }

// Pass in the divider identifier, and this function will, if it can, put a
// map there and setup the map API for use.
//
function setup_map_divider(identifier)
    {

    // If we don't have a compatible browser, indicate that we have a problem.
    // Otherwise, setup the map and its API.
    //
    if (GBrowserIsCompatible())
        {

        // Put a map in the divider area, add some map controls, and set the
        // map to the hybrid mode: part street mode and part satellite imagery
        // mode. Setup the map API.
        //
        map = new GMap2(document.getElementById(identifier));
        map.addControl(new GSmallMapControl());
        map.setMapType(G_HYBRID_MAP);
        map_api = new GClientGeocoder();
        }
    else
        {alert("Your browser is incompatible with map-related features :(");}
    }

// Optionally, pass in the input node of the location component that may have
// changed, and if he location has changed this function will save the node's
// preferences and update any map that we have to the change.
//
function location_change(node)
    {

    // Define a message for our code errors. Get the latitutde, longitude, and
    // address location fields.
    //
    var code_error = "Code error: Location change: ";
    var lat = get_element_value("hidden_latitude");
    var lng = get_element_value("hidden_longitude");
    var new_location = get_element_value("text_address");

    // If the new location is different than the last one, save the
    // preferences for the node that has apparently changed. If we don't have
    // a map, there is nothing else to do. If the map is loaded and the
    // location has not changed, just eject.
    //
    if (new_location != last_location && node)
        {save_pref(node.id);}
    if (( ! map) || (new_location == last_location && map.isLoaded()))
        {return;}

    // Since we have the map, we should also have the map API.
    //
    if ( ! map_api)
        {die(code_error + "Lost the map API");}

    // If we don't have a node, we are just loading up the map. So, if we have
    // a latitutde and longitude, in that case, use them to set the location
    // on the map, regardless of what the text field says.  This is in the
    // case where the user might have dragged the marker, but also works in
    // case the address field was used. If we have a new location, update the
    // map to it as well as our last location variable and lose any info
    // window that we might have for the marker. If we don't have a new
    // location (but have not loaded the map yet), just update the map
    // location to nothing, to indicate that the location is unknown.
    //
    if (lat != "" && lng != "" && ! node)
        {
        var location_obj = new GLatLng(lat, lng);
        update_map_location(location_obj);
        }
    else if (new_location)
        {
        last_location = new_location;
        map_api.getLatLng(new_location, update_map_location);
        map.closeInfoWindow();
        }
    else
        {update_map_location();}}

// If we are given a valid location, update and center the map to it, as well
// as place our map marker there. The boolean dragged indicator is passed
// through to the update map range function.
//
function update_map_location(location_obj, dragged)
    {

    // Define an message for this function's code errors.
    //
    var defaulted;
    var code_error = "Code error: Update map location: ";

    // If we don't have a map that we are trying to update, we have an obvious
    // code error.
    //
    if ( ! map)
        {die(code_error + "I lost the map!");}

    // If we have a location, set the map to it as well as our latitude and
    // longitude input fields.  Otherwise, if the map is not yet loaded, we
    // will need to load it, to a default the location.
    //
    if (location_obj)
        {

        // Update our latitude and longitude input fields to the new location.
        //
        save_node_value("hidden_latitude", location_obj.lat());
        save_node_value("hidden_longitude", location_obj.lng());

        // If the map is not yet loaded, we need to do so by setting its
        // center, so we can pan it in the future. If we have a map marker,
        // set it to the new location as well. Also, update the map range, for
        // the new location, but indicate whether the new location is from
        // being dragged there or not because it likes to know these things.
        //
        if (map.isLoaded()) {map.panTo(location_obj);}
        else {map.setCenter(location_obj, default_map_zoom);}
        if (map_marker) {map_marker.setLatLng(location_obj);}
        update_map_range(dragged);
        }
    else if ( ! map.isLoaded())
        {
        defaulted = true;
        location_obj = new GLatLng(default_lat, default_lng);
        map.setCenter(location_obj, default_map_zoom);
        }

    // Create a map marker if we don't already have one to any location we
    // have. In case we are using the info window for some reason, close it
    // when the marker is dragged. However, at the drag end, make sure we
    // update our map to the new location.
    //
    if (location_obj && ! map_marker)
        {

        // Create a new map marker, that will be shown over any other marker.
        //
        map_marker = new GMarker(location_obj, {draggable: true,
            zIndexProcess: function() {return GOverlay.getZIndex(-90);}});

        // Make sure any open info window is closed when dragging begins.
        //
        GEvent.addListener(map_marker, "dragstart",
            function() {map.closeInfoWindow();});

        // If the user clicks the marker, let them know that it is just the
        // search origin. And, finally, put the marker on the map.
        //
        GEvent.addListener(map_marker, "click",
            function() {map_marker.openInfoWindow("Search origin");});
        map.addOverlay(map_marker);

        // When the drag of the marker is finished, set the last location to
        // the marker location, to prevent location update misunderstandings,
        // and update the map to this new location.
        //
        GEvent.addListener(map_marker, "dragend",
            function()
                {
                last_location = map_marker.getLatLng();
                update_map_location(last_location, true);
                });
        }

    // If we did not get passed in a valid location, then clear our latitude
    // and longitude input fields to indicate this, and put an infobox on the
    // marker to indicate that we do not have a valid location if we were
    // given some attempted location to deal with.
    //
    if (defaulted || ! location_obj)
        {

        // Set latitude and longitude fields, get the text address field
        // value.
        //
        save_node_value("hidden_latitude", "");
        save_node_value("hidden_longitude", "");
        var text_location = get_element_value("text_address");

        // If we don't have a text location, indicate that.
        //
        if (text_location == "")
            {text_location = "&lt;field left blank&gt;"}

        // If the user actually specified an unknown address, let them know
        // about the problem.
        //
        map_marker.openInfoWindowHtml("<b><u>Unknown location:</u></b><br>"
            + "<font color=\"red\">" + text_location + "</font><br>"
            + "<font color=\"green\">(try again or drag marker to the"
            + " location)</font>");
        }
    }

// Optionally, pass in the input node identifier of the location component
// that may have changed, and if the location has changed this function will
// save the node's preferences and update any map that we have to the change.
//
function range_change(node)
    {

    // Define a message for any code errors that we have. We need to have a
    // node to know what range component changed.
    //
    var code_error = "Code error: Range change: ";
    if ( ! node)
        {die(code_error + "No node");}

    // Save the preferences for the node that changed, according to its type.
    //
    if (node.type == "text")
        {save_range_pref(node);}
    else if (node.type == "radio")
        {save_radio_prefs(node);}
    else
        {die(code_error + "Unknown node type: " + node.type);}

    // Finally, we need to update the range on the map.
    //
    update_map_range();
    }

// This function updates the range outline overlay to the current location and
// range.  The zoom of the map is also adjusted if the position was not
// changed from a dragged marker: the passed in variable is a boolean.
//
function update_map_range(dragged)
    {

    // If we don't have an map, just eject, because we cannot update it, of
    // course.
    //
    if ( ! map)
        {return;}

    // Get the range area information, or eject.
    //
    var area_info = get_range_area_info();
    if ( ! area_info)
        {return;}

    // Positive latitude change goes up; positive longitude change goes to the
    // right. Define the four corners of our (square) range from the location.
    //
    var top_left = new GLatLng(area_info.lat_max, area_info.lng_min);
    var top_right = new GLatLng(area_info.lat_max, area_info.lng_max);
    var bottom_left = new GLatLng(area_info.lat_min, area_info.lng_min);
    var bottom_right = new GLatLng(area_info.lat_min, area_info.lng_max);

    // If we had a previous range overlay, just delete it at this point.
    //
    if (range_overlay)
        {map.removeOverlay(range_overlay);}

    // Create a square range outline on the map, using the four points we just
    // made.
    //
    range_overlay = new GPolyline(
        [top_left, top_right, bottom_right, bottom_left, top_left],
        range_box_color, 1, range_box_opacity, {clickable: false});
    map.addOverlay(range_overlay);

    // Appropriately zoom in on the new range. But only if the marker was not
    // dragged to the current location (because that is disturbing).
    //
    if ( ! dragged)
        {

        // Get the perfect zoom level for the map to include the bounds of the
        // range outline we just made. Also, get the current zoom level.
        //
        var bounds = new GLatLngBounds(bottom_left, top_right);
        var perfect_zoom = map.getBoundsZoomLevel(bounds);
        var current_zoom = map.getZoom();

        // If the perfect zoom is not completely zoomed out, use the perfect
        // zoom to make sure the map is zoomed out enough to see the entire
        // range, but don't allow a previous zoom to be too far zoomed out.
        //
        if (perfect_zoom > 1 &&
            (perfect_zoom < current_zoom || current_zoom < perfect_zoom - 1))
            {map.setZoom(perfect_zoom);}}
    }

// This function returns, if possible, an object about the range area, with
// the properties about the range dimensions as well as lat (of the origin of
// the range area), lng, lat_min (mimumum latitude of the range area),
// lat_max, lng_min, and lng_max.
//
function get_range_area_info()
    {

    // If we don't have an map or the current location on it, just eject.
    //
    if ( ! map) {return;}
    var location_obj = get_location();
    if ( ! location_obj) {return;}

    // and get the range in latitude and longitude units, or die.
    //
    var range_area_info = get_range_dimension_info(location_obj);
    if ( ! range_area_info)
        {return;}

    // Break the search location origin into its longitude and latitute
    // components.
    //
    range_area_info.lat = location_obj.lat();
    range_area_info.lng = location_obj.lng();

    // Determine the latitute and longitude minimums and maximums, from our
    // search location origin and the range.
    //
    range_area_info.lat_min = range_area_info.lat - range_area_info.range_lat;
    range_area_info.lat_max = range_area_info.lat + range_area_info.range_lat;
    range_area_info.lng_min = range_area_info.lng - range_area_info.range_lng;
    range_area_info.lng_max = range_area_info.lng + range_area_info.range_lng;

    // Return the range area information that we obtained.
    //
    return range_area_info;
    }

// Pass in the current location, and get back an object about the range
// dimensions, with the properties of range (eg: 20), unit (eg: "km" or "mi"),
// range_lat (range in units of latitudes), range_lng, units_per_lat, and
// units_per_lng.
//
function get_range_dimension_info(location_obj)
    {

    // Define a code error message for this function. If we don't have a
    // location or a map, just eject, because we need to use map-related
    // functionality.
    //
    var code_error = "Code error: Get range dimension info: ";
    if ( ! location_obj || ! map)
        {return;}

    // Try to get the range value, and get the latitutde and longitude
    // components of our location.
    //
    var range = get_element_value("text_range");
    var lat = location_obj.lat();
    var lng = location_obj.lng();

    // If we don't have a range value, we have a code problem.
    //
    if ( ! range)
        {die(code_error + "No new range");}

    // Make the range object that will contain all the information we get
    // about the range, and put in it the range as a number, so we can convert
    // it into meters.
    //
    var range_info = {range: Number(range)};
    var range_meters;

    // If we can't get the range as a number, eject.
    //
    if (isNaN(range_info.range))
        {die(code_error + "Range is not a number");}

    // Convert the range into meters, or die.
    //
    if (document.getElementById("radio_mi").checked)
        {
        range_info.unit = "mi";
        range_meters = range_info.range * meters_per_mi;
        }
    else if (document.getElementById("radio_km").checked)
        {
        range_info.unit = "km";
        range_meters = range_info.range * meters_per_km;
        }
    else
        {die(code_error + "Range unit not identified");}

    // Get our current location with the latitude increased by one, so we can
    // roughly find out the meters to latitude conversion, and then, finally,
    // the range in latitudes.
    //
    var lat_plus_one = new GLatLng(lat + 1,  lng);
    var meters_per_lat = location_obj.distanceFrom(lat_plus_one);
    range_info.range_lat = range_meters / meters_per_lat;

    // Get the range in longitudes.
    //
    var lng_plus_one = new GLatLng(lat, lng + 1);
    var meters_per_lng = location_obj.distanceFrom(lng_plus_one);
    range_info.range_lng = range_meters / meters_per_lng;

    // Add the units per latitude and longitude to our range information
    // object.
    //
    if (range_info.unit == "mi")
        {
        range_info.units_per_lat = meters_per_lat / meters_per_mi;
        range_info.units_per_lng = meters_per_lng / meters_per_mi;
        }
    else if (range_info.unit == "km")
        {
        range_info.units_per_lat = meters_per_lat / meters_per_km;
        range_info.units_per_lng = meters_per_lng / meters_per_km;
        }

    // Return the range dimension information that we obtained.
    //
    return range_info;
    }

// If the latitude and longitude are set, this function returns the GLatLng
// location object of these coordinates.
//
function get_location()
    {

    // Define a message for any code errors we have, and get our current
    // latitude and longitude.
    //
    var code_error = "Code error: Get location: ";
    var latitude = get_element_value("hidden_latitude");
    var longitude = get_element_value("hidden_longitude");

    // If we are missing the latitude or longitude, we can't deal with the
    // range, so eject.
    //
    if (latitude == "" || longitude == "")
        {return;}

    // Define a location variable for the current latitude and longitude
    // coordinates. Then, make easy to work with latitude and longitude
    // numbers.
    //
    var location_obj;
    var lat = Number(latitude);
    var lng = Number(longitude);

    // If we can't get the latitude and the longitude as numbers, eject.
    // Otherwise, set the location object from them.
    //
    if (isNaN(lat) || isNaN(lng))
        {
        die(code_error + "Cannot make latitude (" + latitude
            + ") and longitude (" + longitude + ") a number");
        }
    else
        {location_obj = new GLatLng(lat, lng);}

    // Return the locaiton object that we've created.
    //
    return location_obj;
    }

// Pass in the node of the range input field, and this function makes sure the
// input is just a positive integer.
//
function save_range_pref(node)
    {

    // If the node value is just a number greater than zero, then save the
    // node's value. Otherwise, alert the user how they have an invalid range
    // value and put in something nice.
    //
    if (node.value.match(/^\d+$/) && node.value > 0)
        {save_pref(node.id);}
    else
        {
        alert("Range field can only be an integer from 1 to 999");
        node.value = "20";
        }
    }