/*
 * (c) 2006 David Pritchard
 * All rights reserved.
 */

//document.write('<script src="http://davidpritchard.org/maps/data/expo.js"><\/script>');

function DTransitMarkerIcon(baseurl, name, size, iconAnchor, infoWindowAnchor)
{
    this.name = name;
    this.icon = new google.maps.Icon();
    this.icon.image = baseurl + name + ".png";
    this.icon.shadow = "empty.png";
    this.icon.iconSize = size;
    this.icon.shadowSize = new google.maps.Size(1, 1);
    this.icon.iconAnchor = iconAnchor;
    this.icon.infoWindowAnchor = infoWindowAnchor;
}

function DTransitVertex(xmlElement, baseurl, icons, defaultStop)
{
    this.baseurl = baseurl;
    this.icons = icons;
    this.pos = new google.maps.LatLng(parseFloat(xmlElement.getAttribute("lat")),
                           parseFloat(xmlElement.getAttribute("long")));
    this.label = xmlElement.getAttribute("label");
    this.markerKey = xmlElement.getAttribute("marker")
        ? xmlElement.getAttribute("marker") : defaultStop;
    this.markerZoomLevel = xmlElement.getAttribute("markerzoom")
        ? parseFloat(xmlElement.getAttribute("markerzoom")) : -1;
    this.labelZoomLevel = xmlElement.getAttribute("labelzoom")
        ? parseFloat(xmlElement.getAttribute("labelzoom")) : -1;
    this.labelSize =
        new google.maps.Size(parseFloat(xmlElement.getAttribute("labelwidth")),
                  parseFloat(xmlElement.getAttribute("labelheight")));
    this.labelLocation = xmlElement.getAttribute("labellocation")
        ? xmlElement.getAttribute("labellocation") : "l";
    this.url = xmlElement.getAttribute("url");
    this.address = xmlElement.getAttribute("address");
    this.flags = 0;
    var i;
    for( i = 0; i < icons.length; ++i )
    {
        if( xmlElement.getAttribute(icons[i]) &&
            parseFloat(xmlElement.getAttribute(icons[i])) > 0 )
        {
            this.flags |= 1<<i;
        }
    }
    this.fuse = xmlElement.getAttribute("fuse")
        ? xmlElement.getAttribute("fuse") : null;
    var elems = xmlElement.getElementsByTagName("bus");
    this.buses = "";
    var numBuses = 0;
    for( i = 0; i < elems.length; ++i )
    {
        var busElem = elems[i];
        if( busElem.getAttribute("label") )
        {
            if( numBuses > 0 )
            {
                this.buses += ", ";
            }
            var type = busElem.getAttribute("type")
                ? busElem.getAttribute("type") : "bus";
            this.buses += "<span class=\"" + type + "\">" +
                busElem.getAttribute("label") + "</span>";
            ++numBuses;
        }
    }
    this.notes = xmlElement.getAttribute("notes");
    this.marker = null;
    this.labelIcon = null;
    this.labelMarker = null;

    if( xmlElement.getAttribute("labelkey") )
    {
        this.key = xmlElement.getAttribute("labelkey");
    }
    else if( this.label != null )
    {
        this.key = this.label;
        // Strip out non-alphanumeric
        this.key = this.key.toLowerCase();
        this.key = this.key.replace(/[^a-z0-9]/g, "");
    }
    this.markerAdded = false;
    this.labelAdded = false;
}

DTransitVertex.prototype.createMarker = function(baseurl, markerIcons)
{
    var markerIcon = null;
    for( i = 0; i < markerIcons.length; ++i )
    {
        if( markerIcons[i].name == this.markerKey )
        {
            markerIcon = markerIcons[i].icon;
            break;
        }
    }
    if( null == markerIcon )
    {
        markerIcon = markerIcons[0].icon;
    }
    if( this.label != null && this.marker == null )
    {
        this.marker = new GMarker(this.pos, markerIcon, false);
        google.maps.Event.bind(this.marker, "click", this, this.onMarkerClick);
    }
    if( this.labelSize != null && this.labelMarker == null )
    {
        this.labelIcon = new GIcon();
        this.labelIcon.image = baseurl + "labels/" + this.key + ".png";
        this.labelIcon.shadow = "empty.png";
        this.labelIcon.iconSize = this.labelSize;
        this.labelIcon.shadowSize = new GSize(14, 14);
        if( this.labelLocation == "t" )
        {
            this.labelIcon.iconAnchor =
                new google.maps.Point(this.labelSize.width / 2, this.labelSize.height + 9);
        }
        else if( this.labelLocation == "b" )
        {
            this.labelIcon.iconAnchor =
                new google.maps.Point(this.labelSize.width / 2, -9);
        }
        else if( this.labelLocation == "l" )
        {
            this.labelIcon.iconAnchor =
                new google.maps.Point(this.labelSize.width + 12, this.labelSize.height / 2);
        }
        else if( this.labelLocation == "r" )
        {
            this.labelIcon.iconAnchor =
                new google.maps.Point(-12, this.labelSize.height / 2);
        }
        else if( this.labelLocation == "tl" )
        {
            this.labelIcon.iconAnchor =
                new google.maps.Point(this.labelSize.width + 6, this.labelSize.height + 6);
        }
        else if( this.labelLocation == "tr" )
        {
            this.labelIcon.iconAnchor =
                new google.maps.Point(-6, this.labelSize.height + 6);
        }
        else if( this.labelLocation == "bl" )
        {
            this.labelIcon.iconAnchor =
                new google.maps.Point(this.labelSize.width + 6, -6);
        }
        else if( this.labelLocation == "br" )
        {
            this.labelIcon.iconAnchor =
                new google.maps.Point(-6, -6);
        }
        this.labelIcon.infoWindowAnchor =
            new google.maps.Point(-this.labelSize.width - 5, this.labelSize.height - 1);
        // TODO: speed improvement if inert?
        this.labelMarker = new google.maps.Marker(this.pos, this.labelIcon, false);
        google.maps.Event.bind(this.labelMarker, "click", this, this.onMarkerClick);
    }
}

DTransitVertex.prototype.onMarkerClick = function()
{
    var html;
    if( this.url )
    {
        html = "<a href=\"" + this.url + "\">" + this.label + "</a>";
    }
    else
    {
        html = "<b>" + this.label + "</b>"
    }
    html += "<br/>";
    if( this.address != null )
        html += this.address + "<br/>";
    html += this.buses;
    if( this.notes != null )
        html += "<div class=\"notes\">" + this.notes + "</div>";
    if( this.flags > 0 )
    {
        html = "<table><tr><td valign=\"top\">" + html
            + "</td><td valign=\"top\">";
        for( i = 0; i < this.icons.length; ++i )
        {
            if( this.flags & (1<<i) )
            {
                html += "<img src=\"" + this.baseurl +
                    this.icons[i] + ".png\" /><br/>";
            }
        }
        html += "</td></tr></table>";
    }
    // Wrap in 200px div for Firefox 1.5 and Safari
    html = "<div style=\"width: 200px\">" + html + "</div";
    this.marker.openInfoWindowHtml(html);
}

DTransitVertex.prototype.refreshMarker = function(map, zoomLevel, bounds, visible)
{
    var inBounds = bounds.contains(this.pos);
    var inZoomMarker = zoomLevel >= this.markerZoomLevel &&
        this.markerZoomLevel >= 0;
    var inZoomLabel = zoomLevel >= this.labelZoomLevel &&
        this.labelZoomLevel >= 0;
    if( inBounds && inZoomMarker && visible )
    {
        if( !this.markerAdded && this.marker != null)
        {
            map.addOverlay(this.marker);
            this.markerAdded = true;
        }
    }
    else
    {
        if( this.markerAdded )
        {
            map.removeOverlay(this.marker);
            this.markerAdded = false;
        }
    }
    if( inBounds && inZoomLabel && visible )
    {
        if( !this.labelAdded && this.labelMarker != null)
        {
            //map.addOverlay(this.labelMarker);
            //this.labelAdded = true;
        }
    }
    else
    {
        if( this.labelAdded )
        {
            map.removeOverlay(this.labelMarker);
            this.labelAdded = false;
        }
    }
}

function DTransitRouteLoader()
{
    this.urls = [];
    this.callbacks = [];
    this.data = [];
    this.xmlhttp = null;
}

var the_transitrouteloader = new DTransitRouteLoader();

DTransitRouteLoader.prototype.request = function(url, callback, data)
{
    this.urls.push(url);
    this.callbacks.push(callback);
    this.data.push(data);
    if( this.xmlhttp == null )
    {
        this.open();
    }
}

DTransitRouteLoader.prototype.open = function()
{
  this.xmlhttp = google.maps.XmlHttp.create();
  this.xmlhttp.onreadystatechange = this.onreadystatechange;
  this.xmlhttp.open("GET", this.urls[0], true);
  this.xmlhttp.send(null);
}

DTransitRouteLoader.prototype.onreadystatechange = function()
{
    var rl = the_transitrouteloader;
    if( rl.xmlhttp.readyState == 4)
    {
        var xmlDoc = rl.xmlhttp.responseXML;
        var callback = rl.callbacks[0];
        var data = rl.data[0];
        // Save and remove the data for the completed url.
        rl.urls.shift();
        rl.callbacks.shift();
        rl.data.shift();

        // Call the callback
        callback(data, xmlDoc.documentElement);

        // Start the next one, or signal stop.
        if( rl.urls.length > 0 )
        {
            rl.open();
        }
        else
        {
            rl.xmlhttp = null;
        }
    }
}

function DTransitRoute(baseurl, markerIcons, icons)
{
    this.polylines = [];
    this.baseurl = baseurl;
    this.markerIcons = markerIcons;
    this.icons = icons;
    this.visible = true;
}

DTransitRoute.prototype.onLoad = function(map, xmlElement)
{
    var elems = xmlElement.getElementsByTagName("vertex");
    var vertices = [];
    var i;
    this.defaultStop = xmlElement.getAttribute("defaultstop");
    if( !this.defaultStop )
    {
        this.defaultStop = "stop";
    }
    for( i = 0; i < elems.length; ++i )
    {
        vertices.push(new DTransitVertex(elems[i], this.baseurl, this.icons,
                                         this.defaultStop));
    }
    this.colour = xmlElement.getAttribute("colour");
    this.width = xmlElement.getAttribute("width")
        ? parseFloat(xmlElement.getAttribute("width"))
        : 10;
    this.opacity = xmlElement.getAttribute("opacity")
        ? parseFloat(xmlElement.getAttribute("opacity"))
        : 0.75;

    if( xmlElement.getAttribute("sharedurl") )
    {
        this.sharedOffset = parseFloat(xmlElement.getAttribute("sharedoffset"));
        this.showSharedMarkers = 
            parseFloat(xmlElement.getAttribute("showsharedmarkers"));

        this.sharedMap = map;
        this.vertices = vertices;
        // Flag to indicate "we don't know!"
        this.numSharedVertices = -1;
        the_transitrouteloader.request(
            this.baseurl + xmlElement.getAttribute("sharedurl"),
            this.onSharedLoad, this);
    }
    else
    {
        this.numSharedVertices = 0;
        this.vertices = vertices;
        this.sharedOffset = 0;

        this.createMarkers();
        if( this.visible )
        {
            this.refreshMarkers(map);
            this.showPolylines(map);
        }
    }
}

DTransitRoute.prototype.onSharedLoad = function(cbthis, xmlElement)
{
    var map = cbthis.sharedMap;
    cbthis.sharedMap = null;
    var elems = xmlElement.getElementsByTagName("vertex");
    var sharedVertices = [];
    var i;
    for( i = 0; i < elems.length; ++i )
    {
        sharedVertices.push(new DTransitVertex(elems[i], cbthis.baseurl,
                                               cbthis.icons));
    }
    cbthis.numSharedVertices = sharedVertices.length;
    cbthis.vertices = sharedVertices.concat(cbthis.vertices);
    if( !cbthis.showSharedMarkers )
    {
        var i;
        for( i = 0; i < cbthis.numSharedVertices; ++i )
        {
            cbthis.vertices[i].markerZoomLevel = -1;
            cbthis.vertices[i].labelZoomLevel = -1;
        }
    }
    cbthis.showSharedMarkers = null;

    cbthis.createMarkers();
    if( cbthis.visible )
    {
        // Now that we have them all, create the route.
        cbthis.refreshMarkers(map);
        cbthis.showPolylines(map);
    }
}

DTransitRoute.prototype.createMarkers = function()
{
    var i;
    for( i = 0; i < this.vertices.length; i++ )
    {
        this.vertices[i].createMarker(this.baseurl, this.markerIcons);
    }
}

DTransitRoute.prototype.createPolylines = function(map, widthMult)
{
    var i;
    for( i = 0; i < this.polylines.length; i++ )
    {
        map.removeOverlay(this.polylines[i]);
    }
    this.polylines = [];
    var pointArray;

    pointArray = [];
    for( i = 0; i < this.vertices.length; i++ )
    {
        if( this.vertices[i].fuse != null )
        {
            // Create a line from the pre-fuse points.
            this.createPolyline(map, pointArray, widthMult);
            // Start a new line from the fuse point on.
            pointArray = [];
        }
        pointArray.push(this.vertices[i].pos);
    }
    this.createPolyline(map, pointArray, widthMult);
}

DTransitRoute.prototype.createPolyline = function(map, pointArray, widthMult)
{
    var width = this.width * widthMult;
    if( this.numSharedVertices > 0 )
    {
        // Geographic size
        var span = map.getBounds().toSpan();
        // Pixel size
        var size = map.getSize();
        var pix2latlng = new GLatLng(
            width * this.sharedOffset * span.lat() / size.height,
            width * this.sharedOffset * span.lng() / size.width);
        this.offsetPoints(pointArray, 0, this.numSharedVertices, pix2latlng);
    }
    this.polylines.push(
        new google.maps.Polyline(pointArray, this.colour, width, this.opacity)
    );
}

DTransitRoute.prototype.refreshMarkers = function(map)
{
    var zoom = map.getZoom();
    var center = map.getCenter();
    var span = map.getBounds().toSpan();
    var bigBounds = new GLatLngBounds(
    	new google.maps.LatLng(center.lat() - span.lat() * 1.5,
                    center.lng() - span.lng() * 1.5),
        new google.maps.LatLng(center.lat() + span.lat() * 1.5,
      		    center.lng() + span.lng() * 1.5));
    if( this.vertices )
    {
        for( i = 0; i < this.vertices.length; i++ )
        {
            this.vertices[i].refreshMarker(map, zoom, bigBounds, this.visible);
        }
    }
}

DTransitRoute.prototype.showPolylines = function(map)
{
    if( this.polylines.length == 0 )
    {
        this.createPolylines(map, 1);
    }
    var i;
    for( i = 0; i < this.polylines.length; i++ )
    {
        map.addOverlay(this.polylines[i]);
    }
}

DTransitRoute.prototype.hidePolylines = function(map)
{
    var i;
    for( i = 0; i < this.polylines.length; i++ )
    {
        map.removeOverlay(this.polylines[i]);
    }
}

// Offset points in the range [start,end) by dist, along the point
// normal. Dist is a 2D lat/lng structure.
DTransitRoute.prototype.offsetPoints = function(points, start, end, dist)
{
    var i;
    for(i = start; i < end - 1; i++ )
    {
        var dlat, dlng, len, nlat = 0, nlng = 0, count = 0;
        if( i > 0 )
        {
            dlat = points[i].lat() - points[i-1].lat();
            dlng = points[i].lng() - points[i-1].lng();
            len = Math.sqrt(dlat*dlat+dlng*dlng);
            nlat += dlng / len;
            nlng += -dlat / len;
            ++count;
        }
        if( i+1 < points.length ) 
        {
            dlat = points[i+1].lat() - points[i].lat();
            dlng = points[i+1].lng() - points[i].lng();
            len = Math.sqrt(dlat*dlat+dlng*dlng);
            nlat += dlng / len;
            nlng += -dlat / len;
            ++count;
        }
        if( count > 0 )
        {
            // Average two normals, and renormalize.
            len = Math.sqrt(nlat*nlat+nlng*nlng);
            nlat /= len;
            nlng /= len;
            points[i] = new GLatLng(points[i].lat() + nlat * dist.lat(),
                                    points[i].lng() + nlng * dist.lng());
        }
    }
}

DTransitRoute.prototype.onZoom = function(map, oldZoomLevel, newZoomLevel)
{
    if( this.vertices.length == 0 || this.numSharedVertices < 0 ||
        !this.visible)
        return;

    // Shrink lines at zoom level 9
    if( this.numSharedVertices > 0 ||
        (oldZoomLevel > 9 && newZoomLevel <= 9 ) ||
        (oldZoomLevel <= 9 && newZoomLevel > 9) )
    {
        this.hidePolylines(map);
        // Regenerate polylines for routes with shared vertices after zoom.
        // They may need to be reoffset.
        this.createPolylines(map, newZoomLevel <= 9 ? 0.5 : 1);
        this.showPolylines(map);
    }
    this.refreshMarkers(map);
}

DTransitRoute.prototype.onMoveEnd = function(map)
{
    if( this.vertices.length == 0 || this.numSharedVertices < 0 ||
        !this.visible)
        return;
    this.refreshMarkers(map);
}

DTransitRoute.prototype.hide = function(map)
{
    this.visible = false;
    if( this.vertices )
    {
        this.refreshMarkers(map);
        this.hidePolylines(map);
    }
}

DTransitRoute.prototype.show = function(map)
{
    this.visible = true;
    if( this.vertices )
    {
        this.refreshMarkers(map);
        this.showPolylines(map);
    }
}

DTransitRouteSet = function(map, baseurl, routenames, markerIcons, icons)
{
    this.map = map;
    this.routenames = routenames;
    this.routes = [];
    this.baseurl = baseurl;
    var i;
    for( i = 0; i < this.routenames.length; ++i )
    {
        this.routes.push(new DTransitRoute(baseurl, markerIcons, icons));
    }
    this.numRoutesLoaded = 0;
    for( i = 0; i < this.routenames.length; ++i )
    {
        the_transitrouteloader.request(this.baseurl + this.routenames[i]+".xml",
                                       this.onLoad, this);
    }
}

DTransitRouteSet.prototype.onLoad = function(cbthis, xmlElement)
{
    // this pointer is invalid. cbthis is the real this pointer.
    cbthis.routes[cbthis.numRoutesLoaded].onLoad(map, xmlElement);
    ++cbthis.numRoutesLoaded;
}

DTransitRouteSet.prototype.getRoute = function(routename)
{
    var i;
    for( i = 0; i < this.routenames.length; ++i )
    {
        if( this.routenames[i] == routename )
        {
            return this.routes[i];
        }
    }
    return null;
}

DTransitRouteSet.prototype.onZoom = function(oldZoomLevel, newZoomLevel)
{
    var i;
    for( i = 0; i < this.routes.length; ++i )
    {
        if( this.routes[i] != null )
        {
            this.routes[i].onZoom(this.map, oldZoomLevel, newZoomLevel);
        }
    }
}

DTransitRouteSet.prototype.onMoveEnd = function()
{
    var i;
    for( i = 0; i < this.routes.length; ++i )
    {
        if( this.routes[i] != null )
        {
            this.routes[i].onMoveEnd(this.map);
        }
    }
}

DTransitRouteSet.prototype.hide = function()
{
    var i;
    for( i = 0; i < this.routes.length; ++i )
    {
        if( this.routes[i] != null )
        {
            this.routes[i].hide(this.map);
        }
    }
}