// $Id: map.js,v 1.67 2010/04/12 21:30:46 bask Exp $

var map;
var directions;
var config;

// saved state of currently selected location
// we keep the forecast and weather cached on the client so
// that we can avoid hitting the remote server too often
var state;

var sel_marker;      // Halo for the selected location.
var tc_sel_marker;   // Halo for a nearby tide/current station, when
                     //     the selected location is not tide/current.

var icons    = new Object();
var markers  = new Array();
var wx;                        // Weather prediction, displayed in tide table
var calevent;                  // Calendar entries, displayed in tide table

var mmgrs = new Object();

// Indexed by tid or cid, value is a markers[] index
var tide_marker    = new Array();
var current_marker = new Array();
var aerial_marker;

// Parse the query string from our URL.  Return an associative arrray.
function getQuery()
{
    var query = new Array();

    var qstr = window.location.search.substring(1);
    var pairs = qstr.split("&");
    
    for (var i in pairs) {
        var pair = pairs[i].split("=");
        query[pair[0]] = decodeURIComponent(pair[1]);
    } 

    return query;
}

function marker_types() {
    return ['tide', 'current', 'launch', 'destination', 'meeting', 'aerial'];
}

function type_descr(type) {
    if (type == "launch")      return("Launch Site");
    if (type == "meeting")     return("Meeting Place");
    if (type == "destination") return("Destination");
    if (type == "tide")        return("Tide Station");
    if (type == 'current')     return("Current Station");
}


function color_marker_icon(t) {
    if (config.show[t]) {
        document.getElementById(t + "_icon").src = icons[t].image_base + '.png';
        document.getElementById(t + "_label").style.color = 'black';
    }
    else {
        document.getElementById(t + "_icon").src = icons[t].image_base + '-gray.png';
        document.getElementById(t + "_label").style.color = 'gray';
    }
}

function toggle_markers(t) {
    config.show[t] = !config.show[t];
    color_marker_icon(t);
    if (config.show[t]) mmgrs[t].show();
    else                mmgrs[t].hide();
}

function set_ts_d(n) {
    clear_results();
    config.ts_days[n] = !config.ts_days[n];
    document.getElementById('ts_d' + n).className = 
        config.ts_days[n] ? 'tide_button' : 'tide_button_off';
}

function set_days_noupdate(n) {
    document.getElementById('days').value = n;
    config.days = n;
    tide_radio("days", [ 'h', 1, 2, 4, 7 ], n);
}

function set_days(n) {
    set_days_noupdate(n);
    update_tide();
}

function incr_date(incr, start) { // start is optional - defaults to start_date
    var start_date = incr_date_str(incr, start);

    // This form element is required by the date-picker calendar
    document.getElementById('start_date').value = start_date;

    // Mirror the state in this global variable.
    config.start = start_date;

    set_start_type('');
    update_tide();
}

function set_start_type(type) {
    if (type == '') {
        // Check for today or next Saturday.
        var date = incr_date_str(0);
        if (date == incr_date_str(0, "")) type = 'today';
        else if (config.days == 2 && date == next_sat_str()) type = 'weekend';
    }
    config.start_type = type;
    tide_radio("start_", [ "", "today", "weekend" ], type);
}

// Radio button behavior for tide button strings
function tide_radio(group, vals, v) {
    for (var i in vals) {
        var val = vals[i];
        document.getElementById(group + val).className =
            (v == val) ? 'tide_button' : 'tide_button_off';
    }
}

// Return a date string incremented by the given number of days.
// start_date string is optional - defaults to current date selector.
function incr_date_str(incr, start_date) {

    if (start_date == null) start_date = document.getElementById('start_date').value;
    var today = new Date();
    var date = (start_date == "") ? today : new Date(start_date);

    date = incr_date_obj(incr, date);
    return date.toDateString();
}

function get_date_obj(date_str) {
    return (date_str == "") ? new Date() : new Date(date_str);
}

function incr_date_obj(incr, date) {
    // Set the time to noon, to avoid problems near Daylight Savings transitions.
    date.setHours(12);

    // Increment the date
    var date_ms = date.valueOf();     // milliseconds
    var day_ms = 24 * 60 * 60 * 1000;
    date_ms += incr * day_ms;
    return new Date(date_ms);
}

function set_weekend() {
    // Show two days
    set_days_noupdate(2);

    incr_date(0, next_sat_str());
}

function next_sat_str() {
    // Increment today's date to the next Saturday (i.e. so that it's
    //     day 6 of the week).
    var today = new Date();
    return incr_date_str(6-today.getDay(), "");
}

function set_today() {
    // Show one day
    set_days_noupdate(1);

    incr_date(0, "");
}

function date_from_cal() {
    incr_date(0);
}

// center and zoom the map
// We *must* do this before invoking any other methods on the map object.
function position_map(map)
{
	// don't know why this is necessary, but the zoom is wacky if I don't force it to a #
	var zoom = config.zoom * 1;
	var center = new GLatLng(config.lat, config.lng);
	map.setCenter(center, zoom);

        return map;
}

// Move the info window (if open) to the given marker, but if we're already
//     displaying that marker's info window, close it.
function info_window(index, force_open) {
    if (force_open == 'no_change') return;
    var marker = markers[index];
    var is_hidden = map.getInfoWindow().isHidden();

    if ( ( (force_open || !is_hidden) && state.info_window != index)
                       || ( is_hidden && state.info_window == index) )
    {
        var html = "<div class='iw'><b>"
                   + type_descr(marker.location.type)
                   + ':</b> ' + marker.location.short_title + "</div>";
        marker.openInfoWindowHtml(html, { noCloseOnClick:true });
    }
    else {
        marker.closeInfoWindow();
    }
    state.info_window = index;
    dismiss_save();
}

function pan_to_station(index, alt_index) {
    if (index < 0) { index = alt_index; }
    if (index == null) return;
    var marker = markers[index];
    map.panTo(marker.location.point);
    info_window(index, true);
    aerial_photo(marker.getLatLng());
}

function xtidedate (dateobj, plusmonths) {
    var yyyy = new Number(dateobj.getFullYear());
    var mm   = new Number(dateobj.getMonth()) + 1 + plusmonths;
    if (mm > 12) {
        mm   -= 12;
        yyyy +=  1;
    }
    var dd = dateobj.getDate();   // Might be illegal for mm if > 28.

    return yyyy + '-' + (mm < 10 ? '0' : '') + mm
                + '-' + (dd < 10 ? '0' : '') + dd;
}

var search_results_marker = -1;
function search()
{
	var search_text = document.getElementById("search_text").value.toLowerCase();
        search_text = search_text.replace(/^\s+|\s+$/g, '');
        if (search_text == '') search_text = "(match nothing)";

	var results = "<h3>Search Results</h3>\n";
	var found = new Array();

        search_results_marker = -1;
	
	for (var i = 0; i < markers.length; i++) {
            name = markers[i].location.title.toLowerCase();
            if (name.match(search_text)) found.push(i);
	}

        // Sort the results alphabetically, ignoring case.
        found.sort(function(a,b) {
                       var str_a = markers[a].location.title.toLowerCase();
                       var str_b = markers[b].location.title.toLowerCase();

                       return (str_a <  str_b) ? -1 :
                              (str_a == str_b) ?  0 :
                                                  1 ;
                   });

	if (found.length == 0) results += "No matches found<br/>\n";
        else {
            // Print a link to each match.
            var regex = new RegExp(search_text, "ig");
            results += "<ul>";
            for (var j in found) {
                var i = found[j];

                // Highlight the matching substrings
                var txt = markers[i].location.title.replace(
                              regex, '<span class="srchmatch">$&</span>');

                results += "<li class='"+markers[i].location.type+"_li'>"
                        +  "<a href='javascript:void(0)' onClick='fetch(" + i + ", true, true);'>"
                        +  txt
                        +  "</a>\n";
            }
            results += "</ul>\n";

            // If we found exactly one, fetch it.
            if (found.length == 1) fetch(found[0], true, true);
        }
        document.getElementById("search_results").innerHTML = results;
        search_results_marker = -1;
}

function showtab(id) 
{
	document.getElementById("help_tab").className      = (id == "help") ? "active" : "";
	document.getElementById("weather_tab").className   = (id == "weather") ? "active" : "";
	document.getElementById("info_tab").className      = (id == "info") ? "active" : "";
	document.getElementById("tide_tab").className      = (id == "tide") ? "active" : "";    
	document.getElementById("directions_tab").className = (id == "directions") ? "active" : "";
	document.getElementById("search_tab").className    = (id == "search") ? "active" : "";
	document.getElementById("track_tab").className    = (id == "track") ? "active" : "";
	
	document.getElementById("help").style.display = (id == "help") ? "" : "none";
	document.getElementById("weather").style.display = (id == "weather") ? "" : "none";
	document.getElementById("info").style.display = (id == "info") ? "" : "none";
	document.getElementById("tide").style.display = (id == "tide") ? "" : "none";    
	document.getElementById("directions").style.display = (id == "directions") ? "" : "none";    
	document.getElementById("search").style.display = (id == "search") ? "" : "none";        
	document.getElementById("track").style.display = (id == "track") ? "" : "none";        
        if (id == "directions") {
            document.getElementById("directions_home").focus();
            document.getElementById("directions_home").select();
        }
        else if (id == "search") {
            document.getElementById("search_text").focus();
            document.getElementById("search_text").select();
        }

        config.tab = id;

        toggle_track_visibility(id == "track");

        dismiss_save();
}

// async function to fetch the marine forecast and inject it into the page
updateForecast = function(text) 
{
	var lines = text.split("\n", 2);
	
	// make sure we're looking at a valid forecast file
	if (lines.length == 0 || !lines[0].match("Marine Forecast ")) {
		document.getElementById("forecast").innerHTML = 
			"<strong>ERROR: unable to retrieve the marine forecast at this time. Try again later.</strong>";
	
		// clear current zone id
		state.zone = "";
		return;
	}
	
	// inject the zone forecast into the page
	if (lines[1]) {
		document.getElementById("forecast").innerHTML =
                    "<p class='marine_head'><a target='noaa' href='http://www.wrh.noaa.gov/mtr/'>NOAA Marine Forecast</a></p>"
                    + lines[1];
	}

        // Extract the day forecasts
        wx = new Object();
        for (var i=0; i<7; i++) {
            var wxdiv = document.getElementById("wx"+i);
            wx[i] = (wxdiv != null) ? wxdiv.innerHTML : "";
        }

        update_ttwx();
}

function changeBodyClass(from, to) 
{
	document.body.className = document.body.className.replace(from, to);
	return false;
}

function update_tide() {

    var i = state.tide_curr;

    var tide_img = document.getElementById("tide_img");

    // Blank the graph while we're generating a new graph
    tide_img.src = "images/spinner-tide.gif";
    hide_tide_cursor();
    document.getElementById("tide_list").innerHTML = spinner();
    document.getElementById('tide_alert').style.display = "none";
    document.getElementById("details").innerHTML   = spinner();
    document.getElementById("hrgrids").innerHTML   = '';

    if (i < 0) {

        tc_sel_marker.hide();
        document.getElementById("tc_mkr").src       = 'icons/null.png';
        document.getElementById("tc_sel").src       = 'icons/null.png';
        document.getElementById("tc_txt").innerHTML = 
                "(There is no tide or current data for this location.)";
        document.getElementById("tide_list").innerHTML = "";
        document.getElementById("details").innerHTML   = "";
        tide_img.src = "images/blank-graph.png";
        return;
    }

    var loc = markers[i].location;

    document.getElementById("tc_mkr").src = 'icons/' + loc.type + '.png';
    if (i == state.marker) {
        // The selected marker is a tide/current station.
        tc_sel_marker.hide();
        document.getElementById('tc_sel').src = 'icons/sel.png';
    }
    else {
        // The selected marker is not a tide/current station.
        // Highlight this marker, too.

        // Move the selection icon to the selected point.
        // Increment latitude by a little, to place the image behind
        //     the station marker.
        var latlng = loc.point;
        var sel_point = new GLatLng(latlng.lat()+.00001, latlng.lng());
        tc_sel_marker.setLatLng(sel_point);
        tc_sel_marker.show();
        document.getElementById('tc_sel').src = 'icons/tc_sel.png';
    }

    var title = loc.title;

    // Fetch the tide graph

    var where = encodeURIComponent(title);
    var tcd   = loc.source;
    var width = tide_img.width;
    var start = config.start;
    var days  = config.days;
    var base_url = "tides.php?days=" + days + "&tcd=" + tcd
                   + "&station=" + where
                   + "&type=" + loc.type
                   + "&width=" + width
                   + "&height=" + tide_img.height;
    if (config.start_type != 'today') base_url += '&begin='
                                     + encodeURIComponent(start);
            
    // Display day-of-the-week labels
    var start_wday = get_date_obj(start).getDay();
    for (var i = 0; i < 7; i++) {
        var w = (start_wday + i) % 7;
        var wday_obj = document.getElementById("wday"+w);
        var ndays = config.days;
        if (ndays == 'h') ndays = 1;
        if (i < ndays) {
            wday_obj.style.display = "block";
            // Position each label in the middle of its day's horizontal extent.
            //     The div is 30px wide, so shift it left by 15 to center it.
            wday_obj.style.left = Math.round(width * (i + 0.5) / ndays - 15) + 'px';
        }
        else {
            wday_obj.style.display = "none";
        }
    }

    // Generate a new graph
    tide_img.src = base_url + "&mode=graph";

    var head = (loc.type == "tide") ? "Tide Height: " : "Currents: ";
    var shortname = head + loc.short_title;

    init_ts(state.tide_curr);

    document.getElementById("tide_imgmap").innerHTML = "";

    // Fetch tide/current tables for the Tides tab
    document.getElementById("tc_txt").innerHTML = shortname;

    var url = base_url + "&mode=text";
    GDownloadUrl(url, gettide);
}

function spinner() {
    return "<img class='spinner' src='images/spinner.gif' alt='Loading...' />";
}

gettide = function(raw) {
    // The returned string contains four parts:
    //     * HTML for the tide table (id=tide_list)
    //     * HTML for the tide graph image map area elements.
    //     * HTML for the tide graph grid lines.
    //     * javascript to set the initial tide cursor
    //
    // The latter three parts are returned as HTML comments inside the
    // first part.
    //

    document.getElementById("tide_list").innerHTML = raw;
    update_ttwx();
    update_calevents();

    // Extract the <area> elements HTML from the returned text
    //     and insert them into the existing <map> element.
    var area_map = raw.match(/<!--\s*((<area\b[\s\S]*?\/>)*)\s*-->/);
    document.getElementById("tide_imgmap").innerHTML = area_map[1];

    // Extract the x-axis gridlines from the returned text
    var hrgrids = raw.match(/<!--hrgrids:([\s\S]*?)-->/);
    document.getElementById("hrgrids").innerHTML = hrgrids[1];

    // Kludge: <script>s don't execute on load, so do it explicitly.
    var matches = raw.match(/<script>set_img_cursor\(["']([^"']+)["'],(\d+)\)<\/script>/);

    if (typeof(state.tt_id) == 'undefined' && config.tt_id != '') {
        // This is the first tide table fetch, and we were passed an
        //     initial position for the tide cursor.  Use it.
        if (config.tt_id.match(/ttr\d+_\d+/)) {
            // An entry in the tide table
            set_img_cursor(config.tt_id, config.pos, true);
        }
        else {
            // An entry in the sun/moon table
            set_tide_cursor1(config.tt_id, config.pos);
        }
    }
    else if (matches) {
        set_img_cursor(matches[1],matches[2],true);
    }
}

// A station has been selected.  Fetch data for it, and center the map on it.
function fetch(index, open_info, pan)
{

        if (typeof(index) == "undefined") return;
	var marker = markers[index];
        if (typeof(marker) == "undefined") return;

	var location  = marker.location;
	
        // Close the save dialog
        dismiss_save();

	// center the map on the point
        var latlng = location.point;
	if (pan) map.panTo(latlng);

        // Move the selection halo to the selected point.
        // Increment latitude by a little, to place the image behind
        //     the station marker.
        var sel_point = new GLatLng(latlng.lat()+.00001, latlng.lng());
        sel_marker.setLatLng(sel_point);

        // Change the icon in the title header and Details tab header
        var mkr_img = 'icons/' + location.type + '.png';
        document.getElementById('loc_mkr').src  = mkr_img
        document.getElementById('dir_mkr').src  = mkr_img
        document.getElementById('info_mkr').src = mkr_img
        var sel_img = 'icons/sel.png';
        document.getElementById('loc_sel').src  = sel_img;
        document.getElementById('dir_sel').src  = sel_img;
        document.getElementById('info_sel').src = sel_img;

        if (index != state.marker) clear_directions(null);

	// save the index of this location
	state.marker = index;
	
        var description = type_descr(location.type) + ': ' + location.short_title;
        document.getElementById('loc_txt').innerHTML = description;
	
        document.getElementById('dir_pfx').innerHTML = 
            (location.type == "tide" || location.type == 'current')
                ? "Nearest road to" : '';
        document.getElementById('dir_station').innerHTML = location.short_title;

        document.getElementById("info_txt").innerHTML = description + "<br/>";

	if (location.zone) {
		// don't go get it if we already fetched it.
		// small potential for expired data if the page is left open for a long time
		// we can be smarter and check the expire time later on.
		if (state.zone != location.zone) {
                    // Blank it while we're fetching the new zone's forecast
                    document.getElementById("forecast").innerHTML = spinner();
                    state.zone = location.zone;	
                    wx = null;
                    GDownloadUrl("marine.pl?FORECAST=" + location.zone, updateForecast);
		}
	} else {
                wx = new Object();
		document.getElementById("forecast").innerHTML = 
			"<p><strong>There is no marine forecast for this location.</strong></p>";
		state.zone = 0;
	}
	
        // Find the tide/current station associated with this location.
        state.tide_curr = (location.type == 'tide')    ? index                                    :
	                  (location.type == 'current') ? index                                    :
                          location.tide_station        ? tide_marker[location.tide_station]       :
                          location.current_station     ? current_marker[location.current_station] :
                                                         -1                                       ;

        update_tide();

	if (location.type == 'tide' || location.type == 'current') {
                // Fetch the "about" text for this station for the Details tab
		var where = encodeURIComponent(location.title);
                var tcd   = location.source;
                var tc    = (location.type == "tide") ? "Tide" : "Current";
                var head = encodeURIComponent(tc + " Station: " + location.short_title);
		var about_url = "tides.php?mode=about&tcd=" + tcd + "&station=" + where + "&head=" + head;
		GDownloadUrl(about_url, function(raw){
                    document.getElementById('details').innerHTML = raw;
                });
	} else {   
		var text = location.details ? location.details : "";
                document.getElementById('details').innerHTML =
                    text + "<p><b>Coordinates: </b>" 
                    + location.point.lat() + ', '
                    + location.point.lng();
	}

	if (location.city)
	{
		if (state.weather != location.city) {
			var link = "http://www.wunderground.com/US/CA/" + location.city + ".html";
			var img = 
				"http://banners.wunderground.com/weathersticker/bigwx_cond/language/www/US/CA/" + 
				location.city + ".gif";
			document.getElementById("weather_link").href = link;
			document.getElementById("weather_img").src = img;
			state.weather = location.city;
		}
	}

        info_window(index, open_info);

        aerial_photo(marker.getLatLng());
}

function is_visible(location) {
    return config.show[location.type] == true;
}

function create_station(i, point, location)
{
	var ic;
	ic = get_icon(location.type);
	
        var title  = type_descr(location.type) + ': ' + location.short_title;
        var marker = new GMarker(point, {icon:ic, title:title, draggable:false});
	marker.location = location;
	marker.lid = i;
	markers[i] = marker;
	
	GEvent.addListener(marker, "click", 
            function(latlng) {
                if (config.tab == 'track') marker_click(i, latlng);
                else                       fetch(i, false, true);
            }
	);
	
        return marker;
}

function create_aerial(i, enable) {
    var ic = get_icon('aerial');
    var point = new GLatLng(config.a_lat, config.a_lng);
    var title = 'Drag to move photo viewpoint';
    aerial_marker= new GMarker(point, {icon:ic, title:title, draggable:true});
    GEvent.addListener(aerial_marker, "dragstart",
        function(){
            this.closeInfoWindow();
        } 
    );
    GEvent.addListener(aerial_marker, "dragend",
        function(ll){
            aerial_photo(ll);
        }
    );
    map.addOverlay(aerial_marker, {hide:false}); 
    aerial_change(enable);
}

function load_calevents() {
    GDownloadUrl("calevents.pl", function(raw, rc) {
        if (rc == 200) {
            calevent = new Object();
            eval(raw);
            update_calevents();
        }
    });
}

function load_directions_home() {
    // If we're logged in as a BASK member, initialize the directions
    //   tab "From" address to the member's home address.
    if (window.location.href.match(/[/]members[/]/)) {
        GDownloadUrl("home_addr.pl", function(raw, rc) {
            if (rc == 200) {
                raw = raw.replace(/^\s+/, '');
                raw = raw.replace(/\s+$/, '');
                document.getElementById('directions_home').value = raw;
            }
        });
    }
}

function load_markers()
{
	GDownloadUrl("datapoints.xml", function(data, responseCode) 
	{
		var xml = GXml.parse(data);
		var stations = xml.documentElement.getElementsByTagName("station");

                var markers_by_type = new Object();
		for (var i = 0; i < stations.length; i++) {
		
                        var type = stations[i].getAttribute("station_type");

			var points = stations[i].getElementsByTagName("marker");
			var flow   = stations[i].getElementsByTagName("flow");
			var point  = new GLatLng(parseFloat(points[0].getAttribute("lat")),
			                         parseFloat(points[0].getAttribute("lng")));
			
			var location = new Object;

                        var title = stations[i].getAttribute("title");
                        var short_title = title.replace(/, California( Current)?/, '');
			
			location.id    = i;
			location.point = point;
			location.type = type;
			location.title = title;
			location.short_title = short_title;
			location.zone  = stations[i].getAttribute("zone");
			location.tid   = stations[i].getAttribute("tid");
			location.cid   = stations[i].getAttribute("cid");
			/// leftover?: location.details = stations[i].getElementsByTagName("details")[0];
			location.tide_station = GXml.value(stations[i].getElementsByTagName("tide_station")[0]);
			location.current_station = GXml.value(stations[i].getElementsByTagName("current_station")[0]);
			location.city = GXml.value(stations[i].getElementsByTagName("city")[0]);
			location.details = GXml.value(stations[i].getElementsByTagName("details")[0]);
                        location.source = stations[i].getAttribute("source");

                        if      (location.tid != null)    tide_marker[location.tid] = i;
                        else if (location.cid != null) current_marker[location.cid] = i;
			
			if (flow[0])
			{
				location.ebb   = flow[0].getAttribute("ebb");
				location.flood = flow[0].getAttribute("flood");
			}
			
                        if (typeof(markers_by_type[type]) == 'undefined') {
                            markers_by_type[type] = new Array();
                        }
			var marker = create_station(i, point, location);       
                        markers_by_type[type].push(marker);
		}

                for (var type in markers_by_type) {
                    var mmgr = new MarkerManager(map);
                    mmgrs[type] = mmgr;
                    mmgr.addMarkers(markers_by_type[type], 0);
                    mmgr.refresh();
                    if (config.show[type]) mmgr.show();
                    else                   mmgr.hide();
                }

                sel_marker    = create_sel_marker("icons/sel.png");
                tc_sel_marker = create_sel_marker("icons/tc_sel.png");
                create_aerial(0, config.show['aerial']);
		changeBodyClass('loading', 'standby');

		if (config.location == "" || typeof(markers[config.location] == "undefined"))
                    config.location = 0;

                var pan_on_load = false;
                fetch(config.location, config.info, pan_on_load);

                if (config.tab == 'directions') get_directions();
	});

}

function get_icon(type)
{
    var ic = icons[type];
    return ic.icon;
}

// Create a small icon
function create_icon(type)
{
	
    // Construct image filenames
    var image_base    = "icons/" + type;

    var icon = new GIcon();
    icon.image  = image_base + ".png";
    icon.shadow = image_base + "-shadow.png";
    if (type == 'aerial') {
        icon.iconSize = new GSize(20, 20);
        icon.shadowSize = new GSize(35, 20);
        icon.iconAnchor = new GPoint(10, 10);
        icon.infoWindowAnchor = new GPoint(16, 5);
    }
    else {
        icon.iconSize = new GSize(12, 20);
        icon.shadowSize = new GSize(22, 20);
        icon.iconAnchor = new GPoint(6, 20);
        icon.infoWindowAnchor = new GPoint(9, 5);
    }

    var data = new Object();
    data.type     = type
    data.icon     = icon;
    data.visible  = true;
    data.image_base = image_base;
    icons[type] = data;
}

// Create the icon which will overlay the selected station icon.
function create_sel_marker(image) {
	
    var icon = new GIcon();
    icon.image = image;
    icon.iconSize = new GSize(22, 21);
    icon.iconAnchor = new GPoint(11, 12);

    var sel_marker = new GMarker(new GLatLng(0,0), {icon:icon, draggable:false});

    map.addOverlay(sel_marker, {hide:false}); 

    return sel_marker;
}

function make_icons()
{
    var mtypes = marker_types();
    for (var i in mtypes) create_icon(mtypes[i]);
}

function save_config()
{
	var center = map.getCenter();
	var type = map.getCurrentMapType();
	
	if (type == G_NORMAL_MAP)
		config.mode = 'map';
	else if (type == G_SATELLITE_MAP)
		config.mode = 'sat';
	else if (type == G_HYBRID_MAP)
		config.mode = 'hybrid';
	else if (type == G_PHYSICAL_MAP)
		config.mode = 'phys';
	else
		config.mode = G_NORMAL_MAP;
	
	config.lat = center.lat();
	config.lng = center.lng();
	config.zoom = map.getZoom();

	if (state.marker) {
		config.location = state.marker;
	}
}

// set up initial map params, get from URL if possible
function get_config()
{
    config = new Object;
    config.show = new Array();
    config.show["tide"]        = true;
    config.show["current"]     = true;
    config.show["launch"]      = true;
    config.show["destination"] = true;
    config.show["meeting"]     = true;
    config.show["aerial"]      = false;

    config.ts_days = new Array();
    config.ts_days[6] = true;   // Saturday
    config.ts_days[0] = true;   // Sunday

    config.location = "0";  // The first entry in datapoints.xml

    // Center map on Alcatraz, to show the interesting points around the Bay.
    config.lat =   37.8331;
    config.lng = -122.4165;

    config.zoom = 11;
    config.days = 1;
    config.start = '';
    config.start_type = 'today';
    config.mode = 'phys';
    config.tab  = 'tide';
    config.info = false;      // Hide the info window
    config.tt_id = '';

    // Were we passed any parameters in the URL?  They override
    // anything we did above.
    var query = getQuery();
    var mtypes = marker_types();
    var cursor_pos;
    var start_type = config.start_type;
    for (var key in query) {
        var value = query[key];
        if (key == "mode") {
            config.mode = value;
        }
        else if (key == "z") {
            config.zoom = value;
        }
        else if (key == "ll") {
            var latlng = value.split(",");
            config.lat = latlng[0];
            config.lng = latlng[1];
        }
        else if (key == "a_ll") {
            var latlng = value.split(",");
            config.a_lat = latlng[0];
            config.a_lng = latlng[1];
        }
        else if (key == "marker") {
            config.location = value;
        }
        else if (key == "start") {
            if (value == 'today') {
                config.start = '';
                start_type = 'today';
            }
            else if (value == 'weekend') {
                config.start = incr_date_str(0, next_sat_str());
                start_type = 'weekend';
            }
            else {
                // Convert start time back to verbose format
                var ymd = value.split("-");
                var dobj = new Date();
                dobj.setFullYear(ymd[0]);
                dobj.setMonth(ymd[1]-1);
                dobj.setDate(ymd[2]);
                config.start = dobj.toDateString();
                start_type = '';
            }
        }
        else if (key == "days") {
            config.days = value;
            set_days_noupdate(value);
        }
        else if (key == "tab") {
            config.tab = value;
        }
        else if (key == "hide") {
            for (var i in mtypes) config.show[mtypes[i]] = true;
            var hidden = value.split(",");
            for (var i in hidden) {
                var mtype = hidden[i];
                config.show[mtype] = false;
            }
        }
        else if (key == "pos") {
            config.pos = value;
        }
        else if (key == "tt") {
            config.tt_id = value;
        }
        else if (key == "info") {
            config.info = (value == 1);
        }
        else if (key == "dir") {
            document.getElementById('directions_home').value = value;
        }
    }

    // Initialize date and type in the form
    document.getElementById('start_date').value = config.start;
    set_start_type(start_type);

    if (typeof(config.a_lat) == 'undefined') {
        config.a_lat = config.lat;
        config.a_lng = config.lng;
    }

    showtab(config.tab);

    for (var i in mtypes) {
        var mtype = mtypes[i];
        color_marker_icon(mtype);
    }
}


function setup_map()
{
	var type;
	
        var container = document.getElementById("map");

        // "It is an error to call operations on a newly constructed
        //  GMap2 object until after setCenter() is invoked."
	map = position_map(new GMap2(container));

        map.addMapType(G_PHYSICAL_MAP);
	map.addControl(new GHierarchicalMapTypeControl());
	map.addControl(new GLargeMapControl3D());
	map.addControl(new GScaleControl( ));
        map.addControl(new ExtMapTypeControl({ posRight: 200, showTraffic: true}));
        map.addControl(new GOverviewMapControl());

        // Dismiss the save_page dialog when the map state changes
        GEvent.addListener(map, "moveend",        function(){ dismiss_save() });
        GEvent.addListener(map, "zoomend",        function(){ dismiss_save() });
        GEvent.addListener(map, "maptypechanged", function(){ dismiss_save() });
	
	if (config.mode == 'sat')
		type = G_SATELLITE_MAP;
	else if (config.mode == 'hybrid')
		type = G_HYBRID_MAP;
	else if (config.mode == 'phys')
		type = G_PHYSICAL_MAP;
	else    type = G_NORMAL_MAP;
	
	map.setMapType(type);

        // Create a directions object
        directions = new GDirections(map, document.getElementById("directions_text"));
        GEvent.addListener(directions, "error", function () {
            var errcode = directions.getStatus().code;
            var errmsg =
                (errcode == G_GEO_BAD_REQUEST)         ? "Can't parse request" :
                (errcode == G_GEO_SERVER_ERROR)        ? "Geocode server error" :
                (errcode == G_GEO_MISSING_QUERY)       ? "Missing query" :
                (errcode == G_GEO_UNKNOWN_ADDRESS)     ? 'Unable to locate "' + document.getElementById("directions_home").value + '"' :
                (errcode == G_GEO_UNAVAILABLE_ADDRESS) ? "Unable to give these directions due to legal restrictions" :
                (errcode == G_GEO_UNKNOWN_DIRECTIONS)  ? "Can't find a route between these points" :
                (errcode == G_GEO_TOO_MANY_QUERIES)    ? "We've exceeded our query limit - please try again tomorrow" :
                                                         "Error " + errcode ;
            document.getElementById("directions_status").innerHTML = errmsg;
            document.getElementById("directions_home").focus();
            document.getElementById("directions_home").select();
        });
        GEvent.addListener(directions, "load", function () {
            document.getElementById("directions_status").innerHTML = "";
        });
}

function init()
{
    if (!GBrowserIsCompatible()) {
        document.getElementById("map_app").innerHTML =
            "<td class='error' colspan=2>\n"
            + "Sorry, your browser is not compatible with this Google Maps-based application.\n"
            + "<br>Please visit the Google FAQ page for more information:\n"
            + "<br><a href='http://code.google.com/apis/maps/faq.html#browsersupport' target='gmaps_faq'\n"
            + ">http://code.google.com/apis/maps/faq.html#browsersupport</a>\n"
            + "<hr></td>\n";
        return;
    }

    make_icons();
    get_config();
    var initial_center  = new GLatLng(config.lat, config.lng);
    
    state = new Object();
    state.zone = "";
    state.weather = "";
    state.tide_curr = "";
    state.marker = '';
    state.info_window = -1;

    setup_map();
    map.panTo(initial_center);
    resize_height(100);
    load_directions_home();
    load_markers();
    load_calevents();
    GEvent.addListener(map.getInfoWindow(), "closeclick", info_close);
    ////setup_widgets();
}

function dir_prompt() {
    return "(enter an address or zipcode here)";
}

function home_addr() {
    var home = document.getElementById("directions_home").value;
    home = home.replace(/^\s+/, "");
    if (home == dir_prompt()) home = "";
    return home;
}

// Clear any directions output.  Return the contents of the "from" address
// input element.  If the Directions tab is selected, set focus to the 
// appropriate element.
var is_return;
function clear_directions(is_rtn) {
    if (typeof(is_rtn) != 'undefined') is_return = is_rtn;
    directions.clear();
    document.getElementById('directions_hdr').innerHTML    = "";
    document.getElementById('directions_status').innerHTML = "";

    var home = home_addr();
    if (config.tab != 'directions') return;

    // We're in the Directions tab.  Change the focus as appropriate.

    if (home == "") {
        var home_txt = document.getElementById("directions_home");
        home_txt.value = dir_prompt();
        home_txt.focus();
        home_txt.select();
    }
    else if (is_return == null) {
        document.getElementById("dir_button").focus();
    }
    else {
        document.getElementById(is_return ? "rtn_button" : "dir_button").focus();
    }
}

function get_directions(is_rtn) {

    clear_directions(is_rtn);
    var home = home_addr();
    if (home == "") return;

    document.getElementById('directions_hdr').innerHTML =
        is_rtn ? "Return directions:" : "Directions:";

    document.getElementById('directions_status').innerHTML = spinner();

    // points[0] = origin, points[1] = destination
    var points = new Array();
    points[(is_return ? 1 : 0)] = home;

    var index = state.marker;
    var location = markers[index].location;
    points[(is_return ? 0 : 1)] = location.point.toUrlValue();

    var tgt_sel = document.getElementById("directions_target");
    var dir_options;
    if (tgt_sel.options[tgt_sel.selectedIndex].value == "") {
        dir_options = {};
    }
    else {
        // Don't draw on the trip planner map.
        dir_options = {getPolyline:false, preserveViewport:true};

        var url = 'http://maps.google.com/maps'
                + '?saddr=' + points[0].replace(/\s+/g, '+')
                + '&daddr=' + points[1].replace(/\s+/g, '+');

        var maptype = map.getCurrentMapType().getName();
        if      (maptype == 'Terrain')   url += '&t=p';
        else if (maptype == 'Map')       url += '&t=m';
        else if (maptype == 'Satellite') url += '&t=k';
        else if (maptype == 'Hybrid')    url += '&t=h';

        window.open(url, 'gmaps');
    }
    directions.loadFromWaypoints(points, dir_options);
}

function qstr_from_config (save_date, save_home) {

    save_config();

    var params = new Array();

    var start_date;
    var start_type = save_date ? "" : config.start_type;
    if (start_type != "") {
        start_date = start_type;
    }
    else {
        var dobj = get_date_obj(config.start);
        start_date = dobj.getFullYear() + "-"
                   + (dobj.getMonth() + 1) + "-"
                   + dobj.getDate();
    }

    params.push(
        "mode="     + config.mode,
        "z="        + config.zoom,
        "ll="       + map.getCenter().toUrlValue(4),
        "marker="   + (state.marker || 0),
        "start="    + start_date,
        "days="     + config.days,
        "tab="      + config.tab,
        "tt="       + state.tt_id,
        "pos="      + state.pos,
        "info="     + (map.getInfoWindow().isHidden() ? 0 : 1)
    );

    var hide = new Array();
    var types = marker_types();
    for (var i in marker_types()) {
        var type = types[i];
        if (config.show[type] == false) hide.push(type);
    }
    params.push("hide=" + hide.join(","));

    if (config.show['aerial']) {
        params.push("a_ll=" + aerial_marker.getLatLng().toUrlValue(4));
    }

    if (save_home) {
        var dir = escape(home_addr());
        params.push("dir=" + dir);
    }

    return("?" + params.join("&"));
}

function set_img_cursor(ttr_id, pos, keep_footnote) {
    set_tide_cursor(ttr_id, pos, keep_footnote);
    scroll_to_day(ttr_id);
}

// If it's not visible, scroll the page so this day's data is at the top.
function scroll_to_day(ttr_id) {
    var day_row_id = ttr_id.replace(/(\d+)_(\d+)/, "$1_$1");
    var day_tr = document.getElementById(day_row_id);

    // If there is a scrollbar on the window, scrollIntoView() may
    //     want to scroll the entire window - don't let it.
    // Do it two ways, for browser compatibilty.
    var scroll_before = document.body.scrollTop
                     || document.documentElement.scrollTop;
    day_tr.cells[1].scrollIntoView(true);

    // Restore the window scroll position to what it was before.
    document.body.scrollTop            = scroll_before;
    document.documentElement.scrollTop = scroll_before;
}

// Change the color of the last three columns of the indicated row,
//     and move the cursor.
function set_tide_cursor(ttr_id, pos, keep_footnote) {
    var tr = document.getElementById(ttr_id);
    move_tide_cursor(pos, tr.cells, 3);
    state.tt_id = ttr_id;
    if (!keep_footnote) {
        document.getElementById("tide_footnote").innerHTML = "";
    }
}

// Change the color of the indicated cell, and move the cursor.
// Used for sunrise/sunset links.
function set_tide_cursor1(ttd_id, pos) {
    var td = document.getElementById(ttd_id);
    move_tide_cursor(pos, [td], 1, false);
    state.tt_id = ttd_id;
    document.getElementById("tide_footnote").innerHTML = "";
}

function move_tide_cursor(pos, cells, n) {
    var style = document.getElementById("tide_cursor").style;

    var adj_pos = pos - 1;     // Adjust position for 2-px-wide cursor
    style.left = adj_pos.toString() + "px";

    if (pos < 0) {
        style.display = "none";
    }
    else {
        style.display = "block";
        hl_tide_td(cells, n);
    }
    state.pos = pos;
    dismiss_save();
}

function hide_tide_cursor() {
    move_tide_cursor(-1, [], 0, false);
}

// Change the style of the last n cells in the list.
var hl = new Array();
function hl_tide_td(cells, n) {

    // First turn off any existing highlighting 
    while (hl.length > 0) {
        var td = hl.pop();
        td.className = td.className.replace(/_hl$/, "");
    }

    var l = cells.length;
    if (l >= n) {
        for (var i = l-n; i < l; i++) {
            var td = cells[i];
            td.className = td.className.replace(/(_hl)*$/, "_hl");
            hl.push(td);
        }
    }
}

function save_page() {
    if (config.tab == 'track') {
        alert("Sorry, you can't save track measurements.  Please switch to another tab to use the save function.");
        return;
    }

    document.getElementById('save').style.display = "";
    document.getElementById('save_page').style.display = "none";
    document.getElementById('save_date').innerHTML = incr_date_str(0);

    var home = home_addr();
    var home_display;
    if (home == "") {
        home_display = 'none';
    }
    else {
        document.getElementById('save_home').innerHTML = home;
        home_display = '';
    }
    document.getElementById('save_home_dialog').style.display = home_display;

    var save_date = document.getElementById('save_date_cb').checked;
    var save_home = document.getElementById('save_home_cb').checked;
    var save_tiny = document.getElementById('save_tiny_cb').checked;
    var qstr = qstr_from_config(save_date, save_home);

    // Always link to public page, even from the members-only trip planner.
    var href = window.location.href.replace(/[/]members[/]/, '/');
    href = href.replace(/(?:[?].*)?$/, qstr);

    var save_url = document.getElementById('save_url')

    if (save_tiny) {
        save_url.value = "...wait...";
        document.getElementById('save_link').href = "";

        var tinyurl_req = 'gettinyurl.pl?url=' + escape(href);

        GDownloadUrl(tinyurl_req, function(raw){
            save_url.value = raw;
            document.getElementById('save_link').href = raw;
            save_url.select();
        });
    }
    else {
        save_url.value = href;
        document.getElementById('save_link').href = href;
        save_url.select();
    }
}

function dismiss_save() {
    document.getElementById('save').style.display = "none";
    document.getElementById('save_page').style.display = "";
}

function log_in() {
    var qstr = qstr_from_config(true, true);
    var href = window.location.href;
    window.location = href.replace(/([^/]+[/])(?:[?].*)?$/, "members/$1"+qstr);
}

function aerial_change(enable) {
    if (enable) {
        config.show['aerial'] = true;
        aerial_marker.show();

        // If the marker is not in the current viewport, move
        //     it to the center of the viewport.
        var ll = aerial_marker.getLatLng();
        if (!map.getBounds().contains(ll)) {
            ll = map.getCenter();
        }
        aerial_photo(ll);
    }
    else {
        config.show['aerial'] = false;
        aerial_marker.hide();
        aerial_marker.closeInfoWindow();
    }
    color_marker_icon('aerial');
    dismiss_save();
}

function aerial_image(imagenum) {
    var cgidir = 'http://www.californiacoastline.org/cgi-bin/';
    GDownloadUrl("aerial_coords.pl?image=" + imagenum, move_aerial);
}

function aerial_photo(latlng) {
    aerial_marker.setLatLng(latlng);
    if (!config.show['aerial']) return;
    map.panTo(latlng);
    var lat = latlng.lat();
    var lng = latlng.lng();
    var inside_ggate = (lat > 37.3 && lat < 38.3
                        && lng > -122.48 && lng < -122.23275);

    if (typeof(state.marker) != 'undefined'
        && state.marker != ''
        && latlng.equals(markers[state.marker].location.point)
        && inside_ggate && markers[state.marker].location.zone == 530
    ) {
        // We're inside the Golden Gate.  Don't even attempt to fetch
        //     a photo, lest cacoast move the marker outside the Golden Gate.
        aerial_gray(true);
        return;
    }

    GDownloadUrl("aerial_coords.pl?lat="+lat+";lng="+lng, move_aerial);
}

function move_aerial(raw) {
    // First line of raw is lat,lng,imagenum
    var i = raw.indexOf("\n");
    var data_list = raw.substr(0, i);
    var data = data_list.split(",");

    // The rest is HTML for the infoWindow

    if (data.length < 3) {
        aerial_gray(false);
    }
    else {
        // Move the icon to the place where the photo was taken
        var ll = new GLatLng(data[0], data[1]);
        aerial_marker.setLatLng(ll);

        // Show an active icon
        var imgbase = icons['aerial'].image_base;
        aerial_marker.setImage(imgbase+'.png');

        // Show the aerial thumbnail in the info window.
        aerial_iw(raw.substring(i+1));
    }
}

function cacoast_link() {
    return "<a href='http://www.californiacoastline.org/' "
           + "target='cacoast' " + ">California Coastal Records Project</a>";
}

function aerial_gray(inside_ggate) {
    var imgbase = icons['aerial'].image_base;
    aerial_marker.setImage(imgbase+'-gray.png');
    iw_html = "No aerial photo is available for this location.  The "
              + cacoast_link();
    if (inside_ggate) {
        iw_html += " does not cover the SF Bay shoreline inside the "
                   + "Golden Gate.";
    }
    else {
        iw_html += " covers only the Pacific shoreline of California."
    }
    aerial_iw(iw_html);
}

function aerial_iw(iw_html) {
    var iw_div = "<div class='iw'>" + iw_html + "</div>";
    aerial_marker.openInfoWindowHtml(iw_div, { noCloseOnClick:true });

    state.info_window = -1;   // i.e. not a station marker
}

function info_close() {
    if (state.info_window == -1) {
        aerial_change(false);
    }
}

// Insert BASK calendar events into all of the tide table entries
function update_calevents() {

    var date = get_date_obj(config.start);
    var ndays = config.days;
    if (ndays == 'h') ndays = 1;
    for (var i = 1; i <= ndays; i++) {
        var yyyy = '' + date.getFullYear();
        var mm = '' + (date.getMonth() + 1);
        if (mm.length == 1) mm = '0' + mm;
        var dd = '' + date.getDate();
        if (dd.length == 1) dd = '0' + dd;
        var yyyymmdd = yyyy + mm + dd;

        var html = calevent ? calevent[yyyymmdd]
                            : "...unable to access BASK calendar..."
                            ;
        if (html != null) {
            var caldiv = document.getElementById("cal"+yyyymmdd);
            if (caldiv != null) { caldiv.innerHTML = html; }

            if (html.match(/\bBlue Moon\b/i)) {
                var full_moon_img = document.getElementById("full_moon_img");
                if (full_moon_img != null) { full_moon_img.src = 'images/moon-blue.png'; }
            }
        }

        date = incr_date_obj(1, date);
    }
}

// Insert daily weather predictions into tide table entries
function update_ttwx() {
    for (var i=0; i<7; i++) {
        if (wx && wx[i]) {
            var ttwx = document.getElementById("ttwx"+i);
            if (ttwx) ttwx.innerHTML = wx[i];
        }
    }
    var loc = markers[state.tide_curr].location;
    var alert_txt = loc.source.match(/legacy/)
        ? "<b>WARNING:</b> "+loc.type+" predictions for this station are based on <a href='javascript:showtab("
            + '"info"' + ")' >unofficial data</a>.  Use at your own risk."
        : ""
        ;
    if (!wx) {
        if (alert_txt != "") alert_txt += "<hr>";
        alert_txt += "<b>WARNING:</b> unable to retrieve marine forecast."
    }
    var wxalert = document.getElementById("wxalert");
    var issued;
    // Display the alert on the tide table if the tide table period
    //     overlaps with a 5-day period after the issued time.
    if (wxalert != null) {
        var issued = document.getElementById("wx_issued").innerHTML;
        var issued_ms = Date.parse(issued);
        var day5_ms = issued_ms + 24*60*60*1000 * 5;
        var start_ms;
        if (config.start == '') {
            var today = new Date();
            start_ms = today.getTime();
        }
        else {
            start_ms = Date.parse(config.start);
        }

        var ndays = config.days;
        if (ndays == 'h') ndays = 1;
        end_ms = start_ms + 24*60*60*1000 * ndays;
        if (issued_ms <= end_ms &&  day5_ms >= start_ms) {
            if (alert_txt != "") alert_txt += "<hr>";
            alert_txt += "<a href='javascript:showtab("+'"weather"'+")' title='weather tab'>"
                      + "WEATHER ALERT</a> issued " + issued + ':<br>'
                      + wxalert.innerHTML;
        }
    }

    var tlheight = document.getElementById("tide").offsetHeight - 255;
    var tide_alert = document.getElementById("tide_alert");
    if (alert_txt == '') {
        tide_alert.style.display = 'none';
    }
    else {
        tide_alert.innerHTML = alert_txt;
        tide_alert.style.display = '';
        tlheight -= document.getElementById("tide_alert").offsetHeight;
    }
    // In Firefox 3.5, offsetHeight is 0 when the tide tab is hidden.
    if (tlheight > 0) 
        document.getElementById('tide_list').style.height = tlheight + 'px';
}

function hide_hdr_ftr(hide) {
    var display     = hide ? "none" : "";
    var display_bar = hide ? ""     : "none";
    document.getElementById("header_tr").style.display = display;
    document.getElementById("disclaim").style.display  = display;
    document.getElementById("hide_h_f").style.display  = display;
    document.getElementById("show_h_f").style.display  = display_bar;
    resize_height(100);
}

var resize_timeout;
function resize_height(ms) {
    var tblheight    = document.getElementById("main").clientHeight;
    var windowheight = window_height();
    if (typeof(tblheight)    == "undefined" || tblheight == 0
    ||  typeof(windowheight) == "undefined" || windowheight == 0) return;

    // Compute the new height of the map_box div.
    var delta = windowheight - tblheight;
    var mb = document.getElementById("map_box");
    var mbh = mb.clientHeight + delta;
    var min_mb_height = 350;
    if (mbh < min_mb_height) mbh = min_mb_height;

    // Wait until we haven't had another resize for awhile, then redisplay
    //     tide table, etc.
    if (resize_timeout) clearTimeout(resize_timeout);
    else {
        // This is our first invocation for the current resizing.
        map.savePosition();
    }

    // Default to a half-second
    if (typeof(ms) == 'undefined') ms = 500;

    resize_timeout = setTimeout(function(){

        // Resize the map and all the datapanes
        var h_elems = [
            "map",        "weather", "tide",  "info",
            "directions", "search",  "track", "help",
        ];

        mb.style.height = mbh + "px";
        var content_height = mbh - 36;
        for (var i in h_elems) {
            document.getElementById(h_elems[i]).style.height =
                content_height + "px";
        }
        map.checkResize();

        // Force a refetch of the marker data, to re-render all the panes.
	fetch(state.marker, 'no_change', false);

        map.returnToSavedPosition();
        resize_timeout = false;

    }, ms);
}


// Copied from http://www.howtocreate.co.uk/tutorials/javascript/browserwindow
function window_height() {
  var myHeight = 0;
  if (document.documentElement && document.documentElement.clientHeight) {
    //IE 6+ in 'standards compliant mode'
    // This also works in Safari 4 and Firefox 3.  It includes
    //     the scrollbar, if any.
    myHeight = document.documentElement.clientHeight;
  } else if (typeof(window.innerHeight) == 'number') {
    //Non-IE.  This does not include the scrollbar.
    myHeight = window.innerHeight;
  } else if (document.body && document.body.clientHeight) {
    //IE 4 compatible
    myHeight = document.body.clientHeight;
  }
  return myHeight;
}

var drawing_track;
var track;
function add_track () {

    // In case the user switched tabs during the setTimeout() below.
    if (config.tab != "track") return;

    if (!map) {      // This can happen on startup from a saved link.
        // Wait a while and try again.
        setTimeout("add_track()", 500);
        return;
    }

    if (drawing_track) return;  // Can't delete a track that's being drawn.
    delete_track();
    track = new GPolyline([], "#ff0000", 3, 0.75, {geodesic:true});
    map.addOverlay(track);

    GEvent.addListener(track, "lineupdated", track_updated);
    GEvent.addListener(track, "drag", track_updated);
    GEvent.addListener(track, "click", track_click);
    GEvent.addListener(track, "dragend", vertex_dragged);
    draw_track(false);
    track.enableEditing({onEvent: "mouseover"});
    track.disableEditing({onEvent: "mouseout"});
}

function vertex_dragged() {
    snap_vertex_to_marker(0);
    snap_vertex_to_marker(track.getVertexCount()-1);
}

function snap_vertex_to_marker(v) {
    var latlng = track.getVertex(v);
    var types = ["launch", "destination", "meeting"];
    for (var i in types) {
        var t = types[i];
        // BASK-authored getMatchingMarker() method, based on getMarker().
        var m = mmgrs[t].getMatchingMarker(latlng, true);
        if (m) {
            // Replace the vertex with one that's exactly at the marker.
            track.insertVertex(v, m.getLatLng());
            track.deleteVertex(v+1);
        }
    }
}

var drawing_backwards;
var listen_trackend;
var listen_trackcancel;
function draw_track (backwards) {
    if (drawing_track) return;
    drawing_track = true;
    drawing_backwards = backwards;
    track.enableDrawing({fromStart:backwards});
    listen_trackend    = GEvent.addListener(track, "endline",    end_draw);
    listen_trackcancel = GEvent.addListener(track, "cancelline", end_draw);
}

var ignore_track_click;
function end_draw () {
    ignore_track_click = setTimeout("ignore_track_click = null", 200);
    drawing_track = false;
    if (listen_trackend)    GEvent.removeListener(listen_trackend);
    if (listen_trackcancel) GEvent.removeListener(listen_trackcancel);
    listen_trackend = null;
    listen_trackend = null;
}

function add_point (overlay, newPoint, overlayLatLng) {

    var v;   // Index of added vertex.

    if (track == null) add_track();

    var nv = track.getVertexCount();

    if (drawing_track) v = drawing_backwards ? 0 : nv;
    else if (nv <= 1)  v = nv;    // Add new point at the end of the track.
    else {
        // Add new segment to the nearest endpoint.
        var v0 = track.getVertex(0);
        var vZ = track.getVertex(nv-1);
        drawing_backwards =
            v0.distanceFrom(newPoint) < vZ.distanceFrom(newPoint);
        v = drawing_backwards ? 0 : nv;
    }

    track.insertVertex(v, newPoint);
    track.enableDrawing({fromStart:drawing_backwards});
}

// Update the Track Measurements table.
function track_updated () {
    var nv = track.getVertexCount();

    var v0 = track.getVertex(0);
    var vz = track.getVertex(nv-1);
    var lat0 = v0.lat();
    var latz = vz.lat();
    var lng0 = v0.lng();
    var lngz = vz.lng();

    if (nv == 0) {
        document.getElementById("track_from").innerHTML        = "";
        document.getElementById("track_to").innerHTML          = "";
        document.getElementById("track_from_name").innerHTML   = "";
        document.getElementById("track_to_name").innerHTML     = "";
        document.getElementById("track_distance_mi").innerHTML = "";
        document.getElementById("track_distance_km").innerHTML = "";
        document.getElementById("track_distance_nm").innerHTML = "";
        document.getElementById("track_straight_mi").innerHTML = "";
        document.getElementById("track_straight_km").innerHTML = "";
        document.getElementById("track_straight_nm").innerHTML = "";
    }
    else {
        document.getElementById("track_from").innerHTML =
            (lat0.toFixed(5) + ", " + lng0.toFixed(5));

        document.getElementById("track_to").innerHTML =
            (latz.toFixed(5) + ", " + lngz.toFixed(5));

        var types = ["launch", "destination", "meeting"];
        document.getElementById("track_from_name").innerHTML =
            get_marker_link_at_latlng(v0, types);

        document.getElementById("track_to_name").innerHTML = 
            get_marker_link_at_latlng(vz, types);

        var d = track.getLength();
        var s = vz.distanceFrom(v0);

        document.getElementById("track_distance_mi").innerHTML = (d * 0.0006213711).toFixed(2);
        document.getElementById("track_distance_km").innerHTML = (d * 0.0010000000).toFixed(2);
        document.getElementById("track_distance_nm").innerHTML = (d * 0.0005399568).toFixed(2);
        document.getElementById("track_straight_mi").innerHTML = (s * 0.0006213711).toFixed(2);
        document.getElementById("track_straight_km").innerHTML = (s * 0.0010000000).toFixed(2);
        document.getElementById("track_straight_nm").innerHTML = (s * 0.0005399568).toFixed(2);

    }
}

function delete_track () {
    if (track == null) return;
    if (drawing_track) return;  // Can't delete a track that's being drawn.
    map.removeOverlay(track);
    document.getElementById("track_from").innerHTML = "";
    document.getElementById("track_to").innerHTML = "";
    document.getElementById("track_from_name").innerHTML = "";
    document.getElementById("track_to_name").innerHTML = "";
    document.getElementById("track_distance_mi").innerHTML = "";
    document.getElementById("track_distance_km").innerHTML = "";
    document.getElementById("track_distance_nm").innerHTML = "";
    document.getElementById("track_straight_mi").innerHTML = "";
    document.getElementById("track_straight_km").innerHTML = "";
    document.getElementById("track_straight_nm").innerHTML = "";
    track = null;
    drawing_track = false;
}

function marker_click (i, latlng) {

    if (track != null) {
        for (var v = 0; v < track.getVertexCount(); v++)
            // If the marker is on a vertex, simulate a track click.
            if (latlng.equals(track.getVertex(v)))
                return track_click(latlng, v);
    }
    add_point(markers[i], latlng, latlng);
}

function track_click (latlng, vertex) {

    // Hack: on the final vertex while drawing ends the draw *and*
    //       signals a track click.  Ignore the track click.
    if (ignore_track_click != null) return;

    var nv = track.getVertexCount();

    if (typeof(vertex) == 'undefined') return;

    // Clicking on a point other than an endpoint deletes it.
    if (vertex > 0 && vertex < nv-1) {
        track.deleteVertex(vertex);
        return;
    }

    // Clicked on an endpoint
    if (drawing_track) {
        // Terminate drawing.
        // There is no explicit function for this, but adding and
        //     deleting a vertex seems to work.
        track.insertVertex(nv, latlng);
        track.deleteVertex(nv);
        drawing_track = false;
    }
    else {
        // Resume drawing.
        var backwards = (nv > 1) && (vertex == 0);
        track.deleteVertex(vertex);   // Delete the endpoint
        draw_track(backwards);
    }
}

function toggle_track_visibility(show) {
    if (track) {
        if (show) track.show();
        else      track.hide();
    }
    else if (show) add_track();
}

function get_marker_link_at_latlng (latlng, types) {
    for (var i in types) {
        var t = types[i];

        // BASK-authored getMatchingMarker() method, based on getMarker().
        var m = mmgrs[t].getMatchingMarker(latlng);
        if (m) {
            var txt = m.location.short_title;
            if (txt.length > 32) {
                // Break the string in the middle
                var regex = new RegExp('^(.{'+Math.round(txt.length/2)+'}\\S*) ');
                txt = txt.replace(regex, "$1<br>");
            }
            return "<span class='markername' "
                  + "onclick='pan_to_station(" + m.location.id + ")' >"
                  + "<img class='mkr_img2' align='middle' src='icons/"
                  + t + ".png' /> " + txt + "</span>";
        }
    }
    return "";                // No match
}

function tidetab (date) {
    incr_date(0, date);
    showtab('tide');
}

function tidesearch() {
    var months = new Number(document.getElementById("ts_months").value);
    var startdateobj = ts_start_obj()
    if (!startdateobj) return;
    search_results_marker = state.tide_curr;
    var loc = markers[search_results_marker].location;

    var results = document.getElementById("search_results");

    if (state.tide_curr < 0) {
        results.innerHTML = "There is no tide or current data for this station.";
        return;
    }

    var wdays = '';
    for (var d in config.ts_days) {
        if (config.ts_days[d]) wdays += "," + d;
    }
    if (wdays == '') {
        results.innerHTML = "No days of the week selected.";
        return;
    }

    var url = 'tidesearch.pl'
              + '?begin='   + xtidedate(startdateobj, 0)      + ' 00:00'
              + '&end='     + xtidedate(startdateobj, months) + ' 23:59'
              + '&tcd='     + loc.source
              + '&station=' + encodeURIComponent(loc.title)
              + '&type='    + loc.type
              + '&after='   + document.getElementById("ts_after").value
              + '&before='  + document.getElementById("ts_before").value
              + '&wdays='   + wdays.substr(1)  // Skip the initial ','
              + '&hl_1st='  + document.getElementById("ts_hl_1st").value
              + '&hl_2nd='  + document.getElementById("ts_hl_2nd").value
              + '&hl1a='    + document.getElementById("ts_hl1a").value
              + '&hl1b='    + document.getElementById("ts_hl1b").value
              + '&hl2a='    + document.getElementById("ts_hl2a").value
              + '&hl2b='    + document.getElementById("ts_hl2b").value
              ;
    if (document.getElementById("ts_dl").checked) url += '&exdl=30';

    results.innerHTML = spinner();

    GDownloadUrl(url, function(raw){ results.innerHTML = raw; });
}

function ts_tide_sel (hl_2nd_changed) {
    clear_results();
    return ts_tide_sel_noclear(hl_2nd_changed);
}

function ts_tide_sel_noclear (hl_2nd_changed) {
    var type = markers[state.tide_curr].location.type;
    var hl_1st = document.getElementById("ts_hl_1st");
    var hl_2nd = document.getElementById("ts_hl_2nd");


    var h = (type == 'tide') ? "High Tide" : "Max Flood";
    var l = (type == 'tide') ?  "Low Tide" : "Max Ebb";
    var u = (type == 'tide') ?        "ft" : "kts";

    hl_1st.options[0].text = l;
    hl_1st.options[1].text = h;

    // Second event, if present, must be opposite of first.
    var s1 = hl_1st.selectedIndex;
    hl_2nd.options[1].value = s1 ^ 1;
    hl_2nd.options[1].text  = s1 ? l : h;

    document.getElementById("ts_hl1_units").innerHTML = u;
    document.getElementById("ts_hl2_units").innerHTML = u;

    var hl2a = document.getElementById("ts_hl2a");
    var hl2b = document.getElementById("ts_hl2b");

    var hl2a_div = document.getElementById("ts_hl2a_div");
    var hl2b_div = document.getElementById("ts_hl2b_div");

    if (hl_2nd.selectedIndex == 0) {
        hl2a_div.style.visibility = 'hidden';
        hl2b_div.style.visibility = 'hidden';
    }
    else {
        hl2a_div.style.visibility = 'visible';
        hl2b_div.style.visibility = 'visible';
    }
    ts_tideval_noclear();
}

function clear_results() {
    if (search_results_marker != -1) {
        document.getElementById('search_results').innerHTML = '';
        search_results_marker = -1;
    }
}

function init_ts (i) {
    var loc = markers[i].location;
    document.getElementById("ts_txt").innerHTML = loc.type + 's at ' + loc.short_title;
    ts_tide_sel_noclear();
    ts_date_sel_noclear();

    // -1 means "search_results is not from tidesearch()"
    if (i != search_results_marker && search_results_marker != -1) {
        clear_results();
    }
}

function ts_date_sel () {
    clear_results();
    return ts_date_sel_noclear();
}

function ts_date_sel_noclear () {
    var mon = document.getElementById('ts_mon').value;
    var ts_year_elem = document.getElementById('ts_year');

    if (mon == '') {
        ts_year_elem.style.visibility = 'hidden';
        ts_year_elem.value = '';
    }
    else {
        ts_year_elem.style.visibility = 'visible';
        if (ts_year_elem.value == '') {
            var now = new Date();
            var y = now.getFullYear();
            if (mon < now.getMonth()+1) y++;
            ts_year_elem.value = y;
        }
    }
}

function ts_year_sel () {
    var year_elem = document.getElementById('ts_year');
    var year = year_elem.value;
    clear_results();
    year = year.replace(/^\s+|\s+$/g,'');
    if (!year.match(/^\d\d\d\d$/) || year < 1970 || year > 2035) {
        document.getElementById('search_results').innerHTML =
            "Beginning year ('"+year+"') must be a number between 1970 and 2035.";
        return;
    }
    return year;
}

function ts_time_sel () {
    var after  = document.getElementById('ts_after').value;
    var before = document.getElementById('ts_before').value;
    document.getElementById('ts_dl_nt').innerHTML =
        (after < before) ? 'daylight' : 'nighttime';
    clear_results();
}

function ts_start_obj () {
    var mon =  document.getElementById('ts_mon').value;
    if (mon == '') return new Date();
    else {
        var year = ts_year_sel();
        if (!year) return;
        return new Date(mon + "/1/" + year);
    }
}

function ts_tideval () {
    clear_results();
    ts_tideval_noclear();
}

function ts_tideval_noclear () {
    var ids = [ 'ts_hl1a', 'ts_hl1b', 'ts_hl2a', 'ts_hl2b' ];
    for (var i in ids) {
        var elem = document.getElementById(ids[i]);
        var v = parseFloat(elem.value);
        if (isNaN(v)) v = 0;

        // Max ebb must be negative; max flood must be positive.
        var hl_sel = (i < 2) ? 'ts_hl_1st' : 'ts_hl_2nd';
        var sel_elem = document.getElementById(hl_sel);
        if (markers[state.tide_curr].location.type == 'current') {
            var sel_v = sel_elem.options[sel_elem.selectedIndex].value;
            if (sel_v != '') {
                if (v < 0)      v = -v;        // abs()
                if (sel_v == 0) v = -v;
            }
        }
        elem.value = v;
    }
}
