﻿/// <reference path="MicrosoftAjax.js" />
/// <reference path="jquery-1.4.1.js" />
/// <reference path="google-maps-3-vs-1-0.js" />

Type.registerNamespace("Utils");
Type.registerNamespace("Utils.Map");

Utils.SiteRoot = "./";

//#region Utils.ContextMenu

Utils.ContextMenu =
{
    _events: null,
    _element: null,
    _data: null,

    Show: function (x, y, items)
    {
        /// <summary>
        /// Shows this context menu at the specified coordinates, and populates it with the specified items.
        /// Takes an optional payload that is sent back with events.
        /// </summary>
        /// <param name="x" type="Number" integer="true">The x-coordinate to show the context menu at, relative to the
        /// top left corner of the page.</param>
        /// <param name="y" type="Number" integer="true">The y-coordinate to show the context menu at, relative to the
        /// top left corner of the page.</param>
        /// <param name="items" type="Array" elementType="Object">An array of items to display. Each item must contain
        /// a Text property, and may optionally contain a Value property.</param>
        /// <param name="data" type="Object">The data to store. This data will be sent back in the data property of
        /// arguments passed with events raised by this context menu.</param>

        var e = Function.validateParameters(arguments, [
            { name: "x", type: Number, integer: true },
            { name: "y", type: Number, integer: true },
            { name: "items", type: Array, elementType: Object }
        ]);
        if (e) throw e;

        var itemCount = items.length;
        if (itemCount == 0)
            throw Error.argumentNull("items", "Parameter 'items' must contain elements.");

        // If context menu is currently being displayed, hide it.
        if (Utils.ContextMenu._element)
            Utils.ContextMenu.Hide();

        // Create element markup.
        var innerHTMLParts = [];
        innerHTMLParts.push("<ul>");
        for (var i = 0; i < itemCount; i++)
        {
            innerHTMLParts.push("<li>");
            var item = items[i];
            if (!item.hasOwnProperty("Text"))
                throw Error.argument("items", "Item " + i + " of 'items' argument does not contain 'Text' property.");
            var text = item.Text.toString();
            var value = item.hasOwnProperty("Value") ? item.Value.toString() : text;
            innerHTMLParts.push("<input type=\"hidden\" value=\"" + value + "\" />");
            innerHTMLParts.push("<span>" + text + "</span>");
            innerHTMLParts.push("</li>");
        }
        innerHTMLParts.push("</ul>");

        // Create element, pin it to the document body, and set its position and inner HTML.
        var element = document.createElement("div");
        document.body.appendChild(element);
        var offsetObj = { left: x, top: y };
        jQuery(element).offset(offsetObj).html(innerHTMLParts.join(""));
        jQuery(element).addClass("contextMenu");

        // Set field values.
        Utils.ContextMenu._element = element;

        // Bind click listeners to context-menu list items.
        jQuery("span", element).bind("click", Utils.ContextMenu._item_click);

        // Unbind click listener from body.
        jQuery(document.body).bind("click", Utils.ContextMenu._body_click);

        // Raise shown event.
        Utils.ContextMenu._raiseEvent("shown");
    },

    Hide: function ()
    {
        /// <summary>
        /// Hides this context menu if it is currently showing.
        /// </summary>

        var element = Utils.ContextMenu._element;

        if (!element)
            return;

        // Unbind click listeners from context-menu list items.
        jQuery("span", element).unbind("click");

        // Unbind click listener from body.
        jQuery(document.body).unbind("click");

        // Remove element from the DOM.
        jQuery(element).remove();
        Utils.ContextMenu._element = null;

        // Raise hidden event.
        Utils.ContextMenu._raiseEvent("hidden");
    },

    get_events: function ()
    {
        /// <value type="Sys.EventHandlerList">
        /// This object's event handler list.
        /// </value>

        if (!Utils.ContextMenu._events)
            Utils.ContextMenu._events = new Sys.EventHandlerList();
        return Utils.ContextMenu._events;
    },

    add_shown: function (handler)
    {
        /// <summary>
        /// Adds a handler for when this menu is shown.
        /// </summary>
        /// <param name="handler" type="Function">The handler to add.</param>

        Utils.ContextMenu._addEventHandler("shown", handler);
    },

    remove_shown: function (handler)
    {
        /// <summary>
        /// Removes a handler for when this menu is shown.
        /// </summary>
        /// <param name="handler" type="Function">The handler to remove.</param>

        Utils.ContextMenu._removeEventHandler("shown", handler);
    },

    add_hidden: function (handler)
    {
        /// <summary>
        /// Adds a handler for when this menu is hidden.
        /// </summary>
        /// <param name="handler" type="Function">The handler to add.</param>

        Utils.ContextMenu._addEventHandler("hidden", handler);
    },

    remove_hidden: function (handler)
    {
        /// <summary>
        /// Removes a handler for this object's click event.
        /// </summary>
        /// <param name="handler" type="Function">The handler to remove.</param>

        Utils.ContextMenu._removeEventHandler("hidden", handler);
    },

    add_itemClick: function (handler)
    {
        /// <summary>
        /// Adds a handler for when an item in this menu is clicked.
        /// </summary>
        /// <param name="handler" type="Function">The handler to add.</param>

        Utils.ContextMenu._addEventHandler("itemClick", handler);
    },

    remove_itemClick: function (handler)
    {
        /// <summary>
        /// Removes a handler for when an item in this menu is clicked.
        /// </summary>
        /// <param name="handler" type="Function">The handler to remove.</param>

        Utils.ContextMenu._removeEventHandler("itemClick", handler);
    },

    _body_click: function (evt)
    {
        /// <summary>
        /// Click handler for the document body. Hides the context menu.
        /// </summary>

        var e = Function.validateParameters(arguments, [
            { name: "evt", type: jQuery.Event }
        ]);
        if (e) throw e;

        Utils.ContextMenu.Hide();
    },

    _item_click: function (evt)
    {
        /// <summary>
        /// Click handler for list items. Raises the itemClick event.
        /// </summary>

        var e = Function.validateParameters(arguments, [
            { name: "evt", type: jQuery.Event }
        ]);
        if (e) throw e;

        evt.stopPropagation();

        var value = jQuery("> input[type = 'hidden']:first", evt.target.parentNode).val();
        Utils.ContextMenu._raiseEvent("itemClick", new Utils.ContextMenuItemEventArgs(value));

        Utils.ContextMenu.Hide();
    },

    _addEventHandler: function (eventName, handler)
    {
        /// <summary>
        /// Adds the specified handler for the specified event.
        /// </summary>
        /// <param name="eventName" type="String">The name of the event to add a handler for.</param>
        /// <param name="handler" type="Function">The handler to add.</param>

        var e = Function.validateParameters(arguments, [
            { name: "eventName", type: String },
            { name: "handler", type: Function }
        ]);
        if (e) throw e;

        Utils.ContextMenu.get_events().addHandler(eventName, handler);
    },

    _removeEventHandler: function (eventName, handler)
    {
        /// <summary>
        /// Removes the specified handler for the specified event.
        /// </summary>
        /// <param name="eventName" type="String">The name of the event to remove a handler for.</param>
        /// <param name="handler" type="Function">The handler to remove.</param>

        var e = Function.validateParameters(arguments, [
            { name: "eventName", type: String },
            { name: "handler", type: Function }
        ]);
        if (e) throw e;

        Utils.ContextMenu.get_events().removeHandler(eventName, handler);
    },

    _raiseEvent: function (eventName, eventArgs)
    {
        /// <summary>
        /// Raises the specified event with the specified arguments.
        /// </summary>
        /// <param name="eventName" type="String">The name of the event to raise.</param>
        /// <param name="eventArgs" type="Sys.EventArgs">The argument to pass with the event.</param>

        var e = Function.validateParameters(arguments, [
            { name: "eventName", type: String },
            { name: "eventArgs", type: Sys.EventArgs, mayBeNull: true, optional: true }
        ]);
        if (e) throw e;

        // Retrieve handler for function.
        var handler = Utils.ContextMenu.get_events().getHandler(eventName);
        if (!handler)
            return;

        // If no event argument was provided, default to empty event argument.
        if (!eventArgs)
            eventArgs = Sys.EventArgs.Empty;

        // Raise the event.
        handler(this, eventArgs);
    }
};

//#endregion

//#region Utils.ContextMenuItemEventArgs

Utils.ContextMenuItemEventArgs = function (selectedValue)
{
    /// <summary>
    /// Event arguments passed with the event raised when a context menu item is clicked. Contains the context menu's
    /// selected value.
    /// </summary>
    /// <param name="selectedValue" type="String">The value of the selected context menu item.</param>

    var e = Function.validateParameters(arguments, [
        { name: "selectedValue", type: String }
    ]);
    if (e) throw e;

    this._selectedValue = selectedValue;
};

Utils.ContextMenuItemEventArgs.prototype =
{
    get_selectedValue: function ()
    {
        /// <value type="String">
        /// The value of the item that was selected.
        /// </value>

        return this._selectedValue;
    }
};

Utils.ContextMenuItemEventArgs.registerClass("Utils.ContextMenuItemEventArgs", Sys.EventArgs);

//#endregion

//#region Utils.HelpArticle

Utils.HelpArticle =
{
    Show: function (element)
    {
        var e = Function.validateParameters(arguments, [
            { name: "element", type: Object, domElement: true }
        ]);
        if (e) throw e;

        var content = jQuery(".content", element).html();

        var elementOffset = jQuery(element).offset();
        var elementWidth = jQuery(element).width();
        var windowWidth = jQuery(window).width();

        // Create element, pin it to the document body, and set its position and inner HTML.
        var articleElement = document.createElement("div");
        document.body.appendChild(articleElement);
        var articleQuery = jQuery(articleElement)
        articleQuery.html(content).addClass("helpArticle");

        var articleWidth = jQuery(articleElement).width();

        var offsetLeft = elementOffset.left > articleWidth &&
                                 elementOffset.left + elementWidth + articleWidth > windowWidth
                                    ? elementOffset.left - articleWidth - elementWidth - 20
                                    : elementOffset.left + elementWidth;
        var offsetObj = { left: offsetLeft, top: elementOffset.top };
        articleQuery.offset(offsetObj);

        // Set field values.
        Utils.HelpArticle._element = articleElement;

        // Bind click listener to body.
        $addHandler(document.body, "mousedown", Utils.HelpArticle._body_mousedown);

        // Raise shown event.
        Utils.HelpArticle._raiseEvent("shown");
    },

    Hide: function ()
    {
        var element = Utils.HelpArticle._element;
        if (!element)
            return;

        // Remove element from the DOM.
        jQuery(element).remove();
        Utils.HelpArticle._element = null;

        // Unbind click listener from body.
        $removeHandler(document.body, "mousedown", Utils.HelpArticle._body_mousedown);

        // Raise hidden event.
        Utils.HelpArticle._raiseEvent("hidden");
    },

    get_events: function ()
    {
        /// <value type="Sys.EventHandlerList">
        /// This object's event handler list.
        /// </value>

        if (!Utils.HelpArticle._events)
            Utils.HelpArticle._events = new Sys.EventHandlerList();
        return Utils.HelpArticle._events;
    },

    add_shown: function (handler)
    {
        /// <summary>
        /// Adds a handler for when this help article is shown.
        /// </summary>
        /// <param name="handler" type="Function">The handler to add.</param>

        Utils.HelpArticle._addEventHandler("shown", handler);
    },

    remove_shown: function (handler)
    {
        /// <summary>
        /// Removes a handler for when this help article is shown.
        /// </summary>
        /// <param name="handler" type="Function">The handler to remove.</param>

        Utils.HelpArticle._removeEventHandler("shown", handler);
    },

    add_hidden: function (handler)
    {
        /// <summary>
        /// Adds a handler for when this help article is hidden.
        /// </summary>
        /// <param name="handler" type="Function">The handler to add.</param>

        Utils.HelpArticle._addEventHandler("hidden", handler);
    },

    remove_hidden: function (handler)
    {
        /// <summary>
        /// Removes a handler for when this help article is hidden.
        /// </summary>
        /// <param name="handler" type="Function">The handler to remove.</param>

        Utils.HelpArticle._removeEventHandler("hidden", handler);
    },

    _addEventHandler: function (eventName, handler)
    {
        /// <summary>
        /// Adds the specified handler for the specified event.
        /// </summary>
        /// <param name="eventName" type="String">The name of the event to add a handler for.</param>
        /// <param name="handler" type="Function">The handler to add.</param>

        var e = Function.validateParameters(arguments, [
            { name: "eventName", type: String },
            { name: "handler", type: Function }
        ]);
        if (e) throw e;

        Utils.HelpArticle.get_events().addHandler(eventName, handler);
    },

    _body_mousedown: function (evt)
    {
        if (!jQuery(evt.target).hasClass("helpArticle"))
            Utils.HelpArticle.Hide();
    },

    _raiseEvent: function (eventName, eventArgs)
    {
        /// <summary>
        /// Raises the specified event with the specified arguments.
        /// </summary>
        /// <param name="eventName" type="String">The name of the event to raise.</param>
        /// <param name="eventArgs" type="Sys.EventArgs">The argument to pass with the event.</param>

        var e = Function.validateParameters(arguments, [
            { name: "eventName", type: String },
            { name: "eventArgs", type: Sys.EventArgs, mayBeNull: true, optional: true }
        ]);
        if (e) throw e;

        // Retrieve handler for function.
        var handler = Utils.HelpArticle.get_events().getHandler(eventName);
        if (!handler)
            return;

        // If no event argument was provided, default to empty event argument.
        if (!eventArgs)
            eventArgs = Sys.EventArgs.Empty;

        // Raise the event.
        handler(this, eventArgs);
    },

    _element: null,
    _events: null
};

//#endregion

//#region Utils.Map.LaneType

Utils.Map.LaneType = function ()
{
    /// <summary>
    /// Enumeration of lane types.
    /// </summary>
    /// <field name="Unspecified" type="Number" integer="true">Unspecified lane type.</field>
    /// <field name="Car" type="Number" integer="true">Car lane.</field>
    /// <field name="Truck" type="Number" integer="true">Truck lane.</field>
    /// <field name="Bus" type="Number" integer="true">Bus lane.</field>
    /// <field name="Fast" type="Number" integer="true">FAST lane.</field>
    /// <field name="Nexus" type="Number" integer="true">NEXUS lane.</field>
}

Utils.Map.LaneType.prototype =
{
    Unspecified: 8421504,
    Car: 16711680,
    Truck: 65535,
    Bus: 16776960,
    Fast: 255,
    Nexus: 65280
};

Utils.Map.LaneType.registerEnum("Utils.Map.LaneType", false);

//#endregion

//#region Utils.Map.OverlayView

Utils.Map.OverlayView = function (map)
{
    this.setMap(map);
}

Utils.Map.OverlayView.prototype = new google.maps.OverlayView();

Utils.Map.OverlayView.prototype.draw = function ()
{
    // empty
}

//#endregion

//#region Utils.Map.Map

Utils.Map.Map = function (mapDiv, center, zoomLevel)
{
    var mapOptions =
        {
            center: center,
            zoom: zoomLevel,
            mapTypeId: google.maps.MapTypeId.ROADMAP
        };
    this._innerMap = new google.maps.Map(mapDiv, mapOptions);
    this._overlayView = new Utils.Map.OverlayView(this._innerMap);
};

Utils.Map.Map.prototype =
{
    dispose: function ()
    {
        /// <summary>
        /// Disposes this map.
        /// </summary>

        this._innerMap = null;
    },

    latLngToXY: function (latLng)
    {
        var projection = this._overlayView.getProjection();
        return projection.fromLatLngToContainerPixel(latLng);
    },

    get_innerMap: function ()
    {
        return this._innerMap;
    }
};

Utils.Map.Map.registerClass("Utils.Map.Map", Sys.Component);

//#endregion

//#region Utils.Map.CrossingMarker

Utils.Map.CrossingMarker = function (position, title, draggable, map)
{
    /// <summary>
    /// Initializes a new instance of the CrossingMarker class.
    /// </summary>
    /// <param name="position" Type="google.maps.LatLng">The position at which to draw the marker.</param>
    /// <param name="title" Type="String" mayBeNull="true">The tooltip to display with this marker.</param>
    /// <param name="draggable" Type="Boolean" optional="true">Whether or not the marker is draggable.</param>
    /// <param name="map" Type="google.maps.Map" optional="true" mayBeNull="true">The map to attach the marker
    /// to.</param>

    var e = Function.validateParameters(arguments, [
        { name: "position", type: google.maps.LatLng },
        { name: "title", type: String, mayBeNull: true },
        { name: "draggable", type: Boolean, optional: true },
        { name: "map", type: google.maps.Map, optional: true, mayBeNull: true }
    ]);
    if (e) throw e;

    Utils.Map.CrossingMarker.initializeBase(this);

    // Create marker.
    var options = { position: position, title: title, icon: Utils.SiteRoot + "Content/Images/crossing.png" };
    this._innerMarker = new google.maps.Marker(options);

    if (draggable)
        this.set_draggable(draggable);

    if (map)
        this.set_map(map);
}

Utils.Map.CrossingMarker.prototype =
{
    dispose: function ()
    {
        /// <summary>
        /// Disposes this object.
        /// </summary>

        this._innerMarker = null;

        Utils.Map.CrossingMarker.callBaseMethod(this, "dispose");
    },

    get_innerMarker: function ()
    {
        /// <value type="google.maps.Marker">
        /// The Google Maps marker that this object wraps.
        /// </value>

        return this._innerMarker;
    },

    get_position: function ()
    {
        /// <value type="google.maps.LatLng">
        /// The position of this marker.
        /// </value>

        return this.get_innerMarker().getPosition();
    },

    set_position: function (value)
    {
        var e = Function.validateParameters(arguments, [
            { name: "value", type: google.maps.LatLng }
        ]);
        if (e) throw e;

        if (this.get_position().equals(value))
            return;

        this.get_innerMarker().setPosition(value);

        this.raisePropertyChanged("position");
    },

    get_draggable: function ()
    {
        /// <value type="Boolean">
        /// Whether or not this marker is draggable.
        /// </value>

        return this.get_innerMarker().getDraggable() === true;
    },

    set_draggable: function (value)
    {
        var e = Function.validateParameters(arguments, [
            { name: "value", type: Boolean }
        ]);
        if (e) throw e;

        if (this.get_draggable() === value)
            return;

        this.get_innerMarker().setDraggable(value === true);

        this.raisePropertyChanged("draggable");
    },

    get_map: function ()
    {
        /// <value type="google.maps.Map" mayBeNull="true">
        /// The map on which this marker is placed.
        /// </value>

        return this.get_innerMarker().getMap();
    },

    set_map: function (value)
    {
        var e = Function.validateParameters(arguments, [
            { name: "map", type: google.maps.Map, mayBeNull: true }
        ]);
        if (e) throw e;

        if (this.get_map() == value)
            return;

        this.get_innerMarker().setMap(value);

        this.raisePropertyChanged("map");
    }
};

Utils.Map.CrossingMarker.registerClass("Utils.Map.CrossingMarker", Sys.Component);

//#endregion

//#region Utils.Map.CrossingLaneMarker

Utils.Map.CrossingLaneMarker = function (position, laneType, title, draggable, map)
{
    /// <summary>
    /// Initializes a new instance of the CrossingLaneMarker class.
    /// </summary>
    /// <param name="position" Type="google.maps.LatLng">The position at which to draw the marker.</param>
    /// <param name="laneType" Type="Utils.Map.LaneType">The lane type to use for the marker.</param>
    /// <param name="title" Type="String" mayBeNull="true">The tooltip to display with this marker.</param>
    /// <param name="draggable" Type="Boolean" optional="true">Whether or not the marker is draggable.</param>
    /// <param name="map" Type="google.maps.Map" optional="true" mayBeNull="true">The map to attach the marker
    /// to.</param>

    var e = Function.validateParameters(arguments, [
        { name: "position", type: google.maps.LatLng },
        { name: "laneType", type: Utils.Map.LaneType },
        { name: "title", type: String, mayBeNull: true },
        { name: "draggable", type: Boolean, optional: true },
        { name: "map", type: google.maps.Map, optional: true, mayBeNull: true }
    ]);
    if (e) throw e;

    Utils.Map.CrossingLaneMarker.initializeBase(this);

    this._laneType = null;

    // Create marker.
    var options = { position: position, title: title };
    this._innerMarker = new google.maps.Marker(options);

    this.set_laneType(laneType);

    if (draggable)
        this.set_draggable(draggable);

    if (map)
        this.set_map(map);
}

Utils.Map.CrossingLaneMarker.prototype =
{
    dispose: function ()
    {
        /// <summary>
        /// Disposes this object.
        /// </summary>

        this._innerMarker = null;

        Utils.Map.CrossingLaneMarker.callBaseMethod(this, "dispose");
    },

    get_innerMarker: function ()
    {
        /// <value type="google.maps.Marker">
        /// The Google Maps marker that this object wraps.
        /// </value>

        return this._innerMarker;
    },

    get_position: function ()
    {
        /// <value type="google.maps.LatLng">
        /// The position of this marker.
        /// </value>

        return this.get_innerMarker().getPosition();
    },

    set_position: function (value)
    {
        var e = Function.validateParameters(arguments, [
            { name: "value", type: google.maps.LatLng }
        ]);
        if (e) throw e;

        var oldPosition = this.get_position();

        if (oldPosition.equals(value))
            return;

        this.get_innerMarker().setPosition(value);

        this.raisePropertyChanged("position");
    },

    get_laneType: function ()
    {
        /// <value type="Utils.Map.LaneType">
        /// The lane type of this marker.
        /// </value>

        return this._laneType;
    },

    set_laneType: function (value)
    {
        var e = Function.validateParameters(arguments, [
            { name: "value", type: Utils.Map.LaneType }
        ]);
        if (e) throw e;

        if (this.get_laneType() === value)
            return;

        this._laneType = value;

        var innerMarker = this.get_innerMarker();

        var laneTypeName = null;
        for (var index in Utils.Map.LaneType)
        {
            if (!Utils.Map.LaneType.hasOwnProperty(index))
                continue;

            if (Utils.Map.LaneType[index] != value)
                continue;

            laneTypeName = index;
            break;
        }

        var icon = new google.maps.MarkerImage(
            Utils.SiteRoot + "CrossingLane/Icon?laneType=" + laneTypeName,
            new google.maps.Size(16, 16), null, new google.maps.Point(8, 8));
        innerMarker.setIcon(icon);

//        var innerMarker = this.get_innerMarker();

//        var hexColor = value.toString(16);
//        hexColor = "000000".substring(0, 6 - hexColor.length) + hexColor;

//        var icon = new google.maps.MarkerImage(
//            "https://chart.googleapis.com/chart?chst=d_map_pin_letter&chld=|" + hexColor,
//            new google.maps.Size(20, 34));
//        innerMarker.setIcon(icon);

//        var shadow = new google.maps.MarkerImage(
//            "http://maps.gstatic.com/intl/en_us/mapfiles/markers/marker_sprite.png",
//            new google.maps.Size(37, 34),
//            new google.maps.Point(20, 0),
//            new google.maps.Point(10, 34));

//        innerMarker.setShadow(shadow);

        this.raisePropertyChanged("laneType");
    },

    get_draggable: function ()
    {
        /// <value type="Boolean">
        /// Whether or not this marker is draggable.
        /// </value>

        return this.get_innerMarker().getDraggable() === true;
    },

    set_draggable: function (value)
    {
        var e = Function.validateParameters(arguments, [
            { name: "value", type: Boolean }
        ]);
        if (e) throw e;

        if (this.get_draggable() === value)
            return;

        this.get_innerMarker().setDraggable(value === true);

        this.raisePropertyChanged("draggable");
    },

    get_map: function ()
    {
        /// <value type="Boolean">
        /// Whether or not this marker is draggable.
        /// </value>

        return this.get_innerMarker().getMap();
    },

    set_map: function (value)
    {
        var oldValue = this.get_map();
        if (oldValue == value)
            return;

        this.get_innerMarker().setMap(value);

        this.raisePropertyChanged("map");
    }
};

Utils.Map.CrossingLaneMarker.registerClass("Utils.Map.CrossingLaneMarker", Sys.Component);

//#endregion

//#region Utils.Map.DetectorMarker

Utils.Map.DetectorMarker = function (position, laneType, title, draggable, greyedOut, map, upstreamMarker)
{
    /// <summary>
    /// Initializes a new instance of the DetectorMarker class.
    /// </summary>
    /// <param name="position" Type="google.maps.LatLng">The position at which to draw the marker.</param>
    /// <param name="laneType" Type="Utils.Map.LaneType">The lane type to use for the marker.</param>
    /// <param name="title" Type="String" mayBeNull="true">The tooltip to display with this marker.</param>
    /// <param name="draggable" Type="Boolean" optional="true">Whether or not the marker is draggable.</param>
    /// <param name="map" Type="google.maps.Map" optional="true" mayBeNull="true">The map to attach the marker
    /// to.</param>
    /// <param name="upstreamMarker" Type="Utils.Map.DetectorMarker" optional="true">The upstream detector's
    /// marker.</param>

    var e = Function.validateParameters(arguments, [
        { name: "position", type: google.maps.LatLng },
        { name: "laneType", type: Utils.Map.LaneType },
        { name: "title", type: String, mayBeNull: true },
        { name: "draggable", type: Boolean, optional: true },
        { name: "greyedOut", type: Boolean, optional: true },
        { name: "map", type: google.maps.Map, optional: true, mayBeNull: true },
        { name: "upstreamMarker", type: Utils.Map.DetectorMarker, optional: true, mayBeNull: true }
    ]);
    if (e) throw e;

    Utils.Map.DetectorMarker.initializeBase(this);

    this._laneType = null;
    this._greyedOut = false;
    this._upstreamMarker = null;

    this._outerConnectorPolyline = null;
    this._innerConnectorPolyline = null;

    this._dragstartListener = null;
    this._dragendListener = null;
    this._upstreamDragstartListener = null;
    this._upstreamDragendListener = null;

    this._dragstartDelegate = Function.createDelegate(this, this._marker_dragstart);
    this._dragendDelegate = Function.createDelegate(this, this._marker_dragend);
    this._propertyChangedDelegate = Function.createDelegate(this, this._upstreamMarker_propertyChanged);

    // Create marker.
    // TODO: set marker icon based on lane type.
    var options = { position: position, title: title };
    this._innerMarker = new google.maps.Marker(options);

    // Update icon.
    this._greyedOut = greyedOut === true;
    this.set_laneType(laneType);

    if (draggable)
        this.set_draggable(draggable);

    if (map)
        this.set_map(map);

    if (upstreamMarker)
        this.set_upstreamMarker(upstreamMarker);
}

Utils.Map.DetectorMarker.prototype =
{
    dispose: function ()
    {
        /// <summary>
        /// Disposes this object.
        /// </summary>

        this.set_upstreamDetectorMarker(null);
        this._removeDragListeners();
        this._innerMarker = null;

        Utils.Map.DetectorMarker.callBaseMethod(this, "dispose");
    },

    get_innerMarker: function ()
    {
        /// <value type="google.maps.Marker">
        /// The Google Maps marker that this object wraps.
        /// </value>

        return this._innerMarker;
    },

    get_position: function ()
    {
        /// <value type="google.maps.LatLng">
        /// The position of this marker.
        /// </value>

        return this.get_innerMarker().getPosition();
    },

    set_position: function (value)
    {
        var e = Function.validateParameters(arguments, [
            { name: "value", type: google.maps.LatLng }
        ]);
        if (e) throw e;

        if (this.get_position().equals(value))
            return;

        this.get_innerMarker().setPosition(value);

        if (this.get_map() && this.get_upstreamMarker())
            this._addConnector();

        this.raisePropertyChanged("position");
    },

    get_laneType: function ()
    {
        /// <value type="Utils.Map.LaneType">
        /// The lane type of this marker.
        /// </value>

        return this._laneType;
    },

    set_laneType: function (value)
    {
        var e = Function.validateParameters(arguments, [
            { name: "value", type: Utils.Map.LaneType }
        ]);
        if (e) throw e;

        if (this.get_laneType() === value)
            return;

        this._laneType = value;

        this._updateIcon();

        this.raisePropertyChanged("laneType");
    },

    get_greyedOut: function ()
    {
        /// <value type="Boolean">
        /// Whether or not this detector is greyed out.
        /// </value>

        return this._greyedOut;
    },

    set_greyedOut: function (value)
    {
        var e = Function.validateParameters(arguments, [
            { name: "value", type: Boolean }
        ]);
        if (e) throw e;

        if (this.get_greyedOut() === value)
            return;

        this._greyedOut = value;

        this._updateIcon();

        this.raisePropertyChanged("greyedOut");
    },

    get_draggable: function ()
    {
        /// <value type="Boolean">
        /// Whether or not this marker is draggable.
        /// </value>

        return this.get_innerMarker().getDraggable() === true;
    },

    set_draggable: function (value)
    {
        var e = Function.validateParameters(arguments, [
            { name: "value", type: Boolean }
        ]);
        if (e) throw e;

        if (this.get_draggable() == value)
            return;

        value = (value === true);
        this.get_innerMarker().setDraggable(value);

        // If draggable value changes and connector exists, re-attach drag listeners.
        if (this._hasConnector())
            this._addDragListeners();

        this.raisePropertyChanged("draggable");
    },

    get_map: function ()
    {
        /// <value type="Boolean">
        /// Whether or not this marker is draggable.
        /// </value>

        return this.get_innerMarker().getMap();
    },

    set_map: function (value)
    {
        if (this.get_map() === value)
            return;

        // If connector exists, remove it.
        if (this._hasConnector())
            this._removeConnector();

        // If new map is not null, and upstream marker is not null, and new map equals upstream detector map, draw connector.
        this.get_innerMarker().setMap(value);

        if (value)
        {
            var upstreamMarker = this.get_upstreamMarker();
            if (upstreamMarker && upstreamMarker.get_map() == value)
                this._addConnector();
        }

        this.raisePropertyChanged("map");
    },

    get_upstreamMarker: function ()
    {
        /// <value type="Boolean">
        /// Whether or not this marker is draggable.
        /// </value>

        return this._upstreamMarker;
    },

    set_upstreamMarker: function (value)
    {
        var oldValue = this.get_upstreamMarker();
        if (oldValue == value)
            return;

        if (oldValue)
        {
            // Remove existing connector.
            this._removeConnector();
            oldValue.remove_propertyChanged(this._propertyChangedDelegate);
        }

        this._upstreamMarker = value;

        if (value)
        {
            value.add_propertyChanged(this._propertyChangedDelegate);

            var map = value.get_map();
            if (map && map == this.get_map())
            {
                // If upstream marker is not null and map equals upstream marker's map, draw connector.
                this._addConnector();
            }
        }

        this.raisePropertyChanged("upstreamMarker");
    },

    _updateIcon: function ()
    {
        var innerMarker = this.get_innerMarker();

        var laneType = this.get_laneType();
        var laneTypeName = null;
        var greyedOut = this.get_greyedOut();
        for (var index in Utils.Map.LaneType)
        {
            if (!Utils.Map.LaneType.hasOwnProperty(index))
                continue;

            if (Utils.Map.LaneType[index] != laneType)
                continue;

            laneTypeName = index;
            break;
        }

        var icon = new google.maps.MarkerImage(
            Utils.SiteRoot + "Detector/Icon?laneType=" + laneTypeName + "&isGreyedOut=" + greyedOut,
            new google.maps.Size(16, 16), null, new google.maps.Point(8, 8));
        innerMarker.setIcon(icon);

        //        var hexColor = value.toString(16);
        //        hexColor = "000000".substring(0, 6 - hexColor.length) + hexColor;

        //        var icon = new google.maps.MarkerImage(
        //            "https://chart.googleapis.com/chart?chst=d_map_pin_letter&chld=|" + hexColor,
        //            new google.maps.Size(20, 34));
        //        innerMarker.setIcon(icon);

        //        var shadow = new google.maps.MarkerImage(
        //            "http://maps.gstatic.com/intl/en_us/mapfiles/markers/marker_sprite.png",
        //            new google.maps.Size(37, 34),
        //            new google.maps.Point(20, 0),
        //            new google.maps.Point(10, 34));
        //        innerMarker.setShadow(shadow);

    },

    _marker_dragstart: function ()
    {
        /// <summary>
        /// Handler for when this marker or its upstream marker start being dragged. Hides connector.
        /// </summary>

        // When starting to drag, hide connector, but leave listeners intact.
        this._removeConnector(true);
    },

    _marker_dragend: function ()
    {
        /// <summary>
        /// Handler for when this marker or its upstream marker stop being dragged. Shows connector.
        /// </summary>

        // When done dragging, show connector, and leave listeners intact.
        this._addConnector(true);
    },

    _upstreamMarker_propertyChanged: function (sender, args)
    {
        /// <summary>
        /// Handler for when a property changes on the upstream marker. If property is relevant, adjusts connector
        /// and/or drag listeners.
        /// </summary>

        var propertyName = args.get_propertyName();
        switch (propertyName)
        {
            case "draggable":
                // If upstream marker's draggability has changed, re-connect drag listeners.
                this._addDragListeners();
                break;
            case "position":
            case "map":
                // If upstream marker's position or map has changed, re-draw connector.
                this._removeConnector();
                if (sender.get_map() == this.get_map())
                    this._addConnector();
                break;
        }
    },

    _hasConnector: function ()
    {
        /// <summary>
        /// Returns true if a connector has been drawn on the map.
        /// </summary>

        return this._outerConnectorPolyline != null;
    },

    _addConnector: function (leaveListeners)
    {
        /// <summary>
        /// Adds the connector to the map.
        /// </summary>

        // Remove existing connector.
        this._removeConnector(leaveListeners);

        // Draw connector polylines.
        var map = this.get_map();
        var upstreamMarker = this.get_upstreamMarker();
        //if (!upstreamMarker || upstreamMarker.get_map() !== map)
        //    return;

        var path = [upstreamMarker.get_position(), this.get_position()];
        var outerOptions =
            {
                path: path,
                strokeColor: "#ffffff",
                strokeOpacity: 0.5,
                strokeWeight: 5,
                map: map
            };
        this._outerConnectorPolyline = new google.maps.Polyline(outerOptions);

        var innerOptions =
            {
                path: path,
                strokeColor: "#000000",
                strokeOpacity: 1,
                strokeWeight: 1,
                map: map
            };
        this._innerConnectorPolyline = new google.maps.Polyline(innerOptions);

        // Unless explicitly told not to, re-add drag listeners.
        if (!leaveListeners)
            this._addDragListeners();
    },

    _removeConnector: function (leaveListeners)
    {
        /// <summary>
        /// Removes the connector from the map.
        /// </summary>

        if (!this._hasConnector())
            return;

        if (!leaveListeners)
        {
            // Remove existing drag listeners.
            this._removeDragListeners();
        }

        // Remove connector polylines.
        this._outerConnectorPolyline.setMap(null);
        this._outerConnectorPolyline = null;
        this._innerConnectorPolyline.setMap(null);
        this._innerConnectorPolyline = null;
    },

    _addDragListeners: function ()
    {
        /// <summary>
        /// Adds drag listeners.
        /// </summary>

        this._removeDragListeners();

        if (this.get_draggable())
        {
            this._dragstartListener = google.maps.event.addListener(this.get_innerMarker(), "dragstart", this._dragstartDelegate);
            this._dragendListener = google.maps.event.addListener(this.get_innerMarker(), "dragend", this._dragendDelegate);
        }

        var upstreamMarker = this.get_upstreamMarker();
        if (upstreamMarker && upstreamMarker.get_draggable())
        {
            this._upstreamDragstartListener = google.maps.event.addListener(upstreamMarker.get_innerMarker(), "dragstart", this._dragstartDelegate);
            this._upstreamDragendListener = google.maps.event.addListener(upstreamMarker.get_innerMarker(), "dragend", this._dragendDelegate);
        }
    },

    _removeDragListeners: function ()
    {
        /// <summary>
        /// Removes drag listeners.
        /// </summary>

        // Remove any existing drag-start or drag-end listeners.
        if (this._dragstartListener)
        {
            google.maps.event.removeListener(this._dragstartListener);
            this._dragstartListener = null;
        }
        if (this._dragendListener)
        {
            google.maps.event.removeListener(this._dragendListener);
            this._dragendListener = null;
        }
        if (this._upstreamDragstartListener)
        {
            google.maps.event.removeListener(this._upstreamDragstartListener);
            this._upstreamDragstartListener = null;
        }
        if (this._upstreamDragendListener)
        {
            google.maps.event.removeListener(this._upstreamDragendListener);
            this._upstreamDragendListener = null;
        }
    }
};

Utils.Map.DetectorMarker.registerClass("Utils.Map.DetectorMarker", Sys.Component);

//#endregion

function crossingIndexSidebarElement_change(evt)
{
    if (Sys.UI.DomElement.containsCssClass(evt.target, "selectAll"))
    {
        // If select-all checkbox was checked, check all other checkboxes in sidebar.
        var checked = evt.target.checked;
        var sidebarElement = evt.target.parentNode.parentNode;
        var checkBoxes = jQuery("input[type = 'checkbox']", sidebarElement);
        var checkBoxCount = checkBoxes.length;
        for (var i = 0; i < checkBoxCount; i++)
        {
            checkBoxes.get(i).checked = checked;
        }
    }
    else if (Sys.UI.DomElement.containsCssClass(evt.target, "dataSource"))
    {
        // If data-source drop-down list value changed, set data source and year and month hidden field values.
        var value = evt.target.value;
        var dataSourceIdHiddenField = document.getElementById("DataSourceID");
        var isLaneHiddenField = document.getElementById("DataSourceIsLane");
        var directionOfTravelHiddenField = document.getElementById("DirectionOfTravel");

        var patternMatch = /^c_(\d+)$/.exec(value);
        if (patternMatch != null)
        {
            dataSourceIdHiddenField.value = patternMatch[1];
            isLaneHiddenField.value = "false";
            directionOfTravelHiddenField.value = "";
        }
        else
        {
            patternMatch = /^c_(\d+)_(\w+)$/.exec(value);
            if (patternMatch != null)
            {
                dataSourceIdHiddenField.value = patternMatch[1];
                isLaneHiddenField.value = "false";
                directionOfTravelHiddenField.value = patternMatch[2];
            }
            else
            {
                patternMatch = /^g_(\d+)$/.exec(value);
                if (patternMatch != null)
                {
                    dataSourceIdHiddenField.value = patternMatch[1];
                    isLaneHiddenField.value = "true";
                    directionOfTravelHiddenField.value = "";
                }
            }
        }

        var sidebarElement = evt.target.parentNode.parentNode;

        var yearHiddenField = jQuery(".year input[type = 'hidden']", sidebarElement).get(0);
        var monthHiddenField = jQuery(".month input[type = 'hidden']", sidebarElement).get(0);

        if (yearHiddenField.value == "")
        {
            var date = new Date();
            yearHiddenField.value = date.getFullYear().toString();
            monthHiddenField.value = (date.getMonth() + 1).toString();
        }
    }
    else if (evt.target.tagName.toLowerCase() == "select")
    {
        // When any drop-down list value changes, update the corresponding hidden field value.
        var hiddenFieldQuery = jQuery("input[type = 'hidden']", evt.target.parentNode);
        if (hiddenFieldQuery.length > 0)
            hiddenFieldQuery.get(0).value = evt.target.value;
    }

    // Return output format to table, in case it has changed.
    var formatHiddenField = document.getElementById("Format");
    if (formatHiddenField != null)
        formatHiddenField.value = "table";

    // Force form submit.
    evt.target.form.submit();
}

function detectorIndexSidebarElement_change(evt)
{
    if (Sys.UI.DomElement.containsCssClass(evt.target, "selectAll"))
    {
        // If select-all checkbox was checked, check all other checkboxes in sidebar.
        var checked = evt.target.checked;
        var sidebarElement = evt.target.parentNode.parentNode;
        var checkBoxes = jQuery("input[type = 'checkbox']", sidebarElement);
        var checkBoxCount = checkBoxes.length;
        for (var i = 0; i < checkBoxCount; i++)
        {
            checkBoxes.get(i).checked = checked;
        }
    }
    else if (Sys.UI.DomElement.containsCssClass(evt.target, "dataSource"))
    {
        // If data-source drop-down list value changed, set both year and month value if no year has been set.
        var sidebarElement = evt.target.parentNode.parentNode;

        var yearHiddenField = jQuery(".year input[type = 'hidden']", sidebarElement).get(0);
        var monthHiddenField = jQuery(".month input[type = 'hidden']", sidebarElement).get(0);

        if (yearHiddenField.value == "")
        {
            var date = new Date();
            yearHiddenField.value = date.getFullYear().toString();
            monthHiddenField.value = (date.getMonth() + 1).toString();
        }
    }

    if (evt.target.tagName.toLowerCase() == "select")
    {
        // When any drop-down list value changes, update the corresponding hidden field value.
        var hiddenFieldQuery = jQuery("input[type = 'hidden']", evt.target.parentNode);
        if (hiddenFieldQuery.length > 0)
            hiddenFieldQuery.get(0).value = evt.target.value;
    }

    // Return output format to table, in case it has changed.
    var formatHiddenField = document.getElementById("Format");
    if (formatHiddenField != null)
        formatHiddenField.value = "table";

    // Force form submit.
    evt.target.form.submit();
}

function frieghtIndexSidebarElement_change(evt)
{
    if (Sys.UI.DomElement.containsCssClass(evt.target, "selectAll"))
    {
        // If select-all checkbox was checked, check all other checkboxes in sidebar.
        var checked = evt.target.checked;
        var checkBoxes = jQuery("input[type = 'checkbox']");
        var checkBoxCount = checkBoxes.length;

        for (var i = 0; i < checkBoxCount; i++)
        {
            checkBoxes.get(i).checked = checked;
        }
    }

    if (Sys.UI.DomElement.containsCssClass(evt.target, "commodityItem"))
    {
        // If select-all option was selected and one commodity item checkbox was unchecked, uncheck select-all option in sidebar.
        var checked = evt.target.checked;
        var checkBoxes = jQuery("input[type = 'checkbox'].selectAll");
        var checkBoxCount = checkBoxes.length;

        if (!checked && checkBoxCount > 0)
        {
            for (var i = 0; i < checkBoxCount; i++)
            {
                checkBoxes.get(i).checked = checked;
            }
        }
    }

    if (evt.target.tagName.toLowerCase() == "select") {
        // When any drop-down list value changes, update the corresponding hidden field value.
        var hiddenFieldQuery = jQuery("input[type = 'hidden']", evt.target.parentNode);
        if (hiddenFieldQuery.length > 0)
            hiddenFieldQuery.get(0).value = evt.target.value;
    }

    // Return output format to table, in case it has changed.
    var formatHiddenField = document.getElementById("Format");
    if (formatHiddenField != null)
        formatHiddenField.value = "table";

    // Force form submit.
    evt.target.form.submit();
}

function formatTab_click(evt)
{
    var classNames = evt.target.className.split(" ");

    var classNameCount = classNames.length;
    for (var i = 0; i < classNameCount; i++)
    {
        var className = classNames[i];
        if (className.substring(0, 4) != "tab-")
            continue;

        var hiddenField = document.getElementById("Format");
        hiddenField.value = className.substring(4);
        hiddenField.form.submit();
        break;
    }
}

function crossingDeleteButton_click(evt)
{
    var data = evt.data;
    var message = "Certain?";
    if (data.hasOwnProperty("message") && data.message != null && data.message !== "")
    {
        message = data.message;
    }

    if (!confirm(message))
        return;

    var goToDeleteHiddenField = document.getElementById("GoToDelete");
    goToDeleteHiddenField.value = "true";
    goToDeleteHiddenField.form.submit();
}

function formatCsvButton_click(evt)
{
    var formatHiddenField = document.getElementById("Format");
    formatHiddenField.value = "csv";
    formatHiddenField.form.submit();
}
