/**
 *
 * @project     fcmq2009
 * @purpose     contains the "Routing" module
 * @author      Alexandre Dubé - Mapgears inc. (adube@mapgears.com)
 * @copyright
 * <b>Copyright (c) 2009 Mapgears Inc.</b>
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 */

/**
 * Class: RoutingControl
 * Used as a class to create the Routing object.
 *
 * Parameters:
 * options - {Object} An object of options used to create the Routing object.
 */
function RoutingControl(map, main, distCalc, options) {
    this.map = map;
    this.main = main;
    this.distanceCalculator = distCalc;
    this.options = options;
};

RoutingControl.prototype = new GControl();

/**
 * Method: initialize
 *
 * Add a singlerightclick listener to be able to remove 'added' markers and
 *     reset the corresponding initMarker.  Create the div container on map.
 *
 * MarkerTypes :
 *   1: start
 *   2: end
 *   6: relay
 *   7: tempRelay
 */
RoutingControl.prototype.initialize = function(map) {
    this.directionTableClassName = null;
    this.directionProperty = null;
    this.directionDivTableContainerId = null;
    this.angleProperty = null;
    this.angleFollowProperty = null;
    this.cardinalDirections = null;
    this.cardinalDirectionsOrder = null;
    this.turnDirections = null;
    this.turnDirectionsOrder = null;
    this.numberProperty = null;
    this.tempRelayMarker = null;
    this.mapMouseMove = null;
    this.startIdProperty = null;
    this.endIdProperty = null;
    this.routingRatio = null;
    this.directionTitle = null;
    this.startMarkerType = null;
    this.endMarkerType = null;
    this.relayMarkerType = null;
    this.tempRelayMarkerType = null;
    
    this.draggingMarker = false;

    /**
     * Property: routingQueries
     * {Integer} The number of routing queries currently in progress.
     */
    this.routingQueries = 0;
    
    this.markers = [];
    this.initMarkers = [];
    this.routingOverlays = [];

    this.relayIndex = null;

    OpenLayers.Util.extend(this, this.options);

    this.directionTableContainer = 
        document.getElementById(this.directionDivTableContainerId);
    this.directionDiv = document.getElementById(this.directionDivId);
    this.transQuebecOnly = document.getElementById(this.transQuebecCheckBox);
    this.autoRecenter = document.getElementById(this.autoRecenterCheckBox);
    this.directionTitleDiv = document.getElementById(this.directionTitle);

    GEvent.bind(this.map,'singlerightclick',this,this.onSingleRightClick);

    // the div container on the map
    var container = document.createElement("div");

    // bug, doesn't work in 2.149
    /*
    var oChild = document.getElementById("divRoutingMenu");
    oChild.style.border = "2px solid black";
    container.appendChild(oChild);
    */

    this.map.getContainer().appendChild(container);
    return container;
};

/**
 * Method: getDefaultPosition
 * Sets the default position in the map of this control.  Uses the global
 *     "options" variable to do so, "routingDiv" must be set.  See options.js
 *
 * Returns:
 * {<GControlPosition>}
 */
RoutingControl.prototype.getDefaultPosition = function() {
    var oSize;
    if(options && options.routingDiv &&
       options.routingDiv.offsetx && options.routingDiv.offsety) {
        oSize = new GSize(
            options.routingDiv.offsetx,
            options.routingDiv.offsety
        );
    } else {
        oSize = new GSize(70, 3);
    }
    
  return new GControlPosition(G_ANCHOR_TOP_LEFT, oSize);
};

/**
 * Method: createInitMarker
 * Called on initialization.  Creates the 2 init marker objects and put them
 *     in the initMarkers array.
 */
RoutingControl.prototype.createInitMarkers = function() {
    var oStartInitMarker = this.createInitMarker(this.startMarkerType);
    this.initMarkers.push(oStartInitMarker);
    this.map.addOverlay(oStartInitMarker);

    var oEndInitMarker = this.createInitMarker(this.endMarkerType);
    this.initMarkers.push(oEndInitMarker);
    this.map.addOverlay(oEndInitMarker);
};

/**
 * Method: createInitMarker
 * Creates an init marker object given a specific type.
 *
 * Parameters:
 * nMarkerType - {Integer} The marker type
 *
 * Returns:
 * {<GMarker>}
 */
RoutingControl.prototype.createInitMarker = function(nMarkerType) {
    var oLatLng = this.getLatLngFromMarkerType(nMarkerType);
    var oMarker = this.createMarker(nMarkerType, oLatLng);

    GEvent.bind(oMarker, "dragstart", oMarker, this.onInitMarkerDragStart);
    GEvent.bind(oMarker, "dragend", oMarker, this.onInitMarkerDragEnd);
    GEvent.bind(this.map, "move", oMarker, this.onMapMove);

    return oMarker;
};

/**
 * Method: createMarker
 * Creates a marker object.  Set its 'style' depending on its 'markerType'
 *     property.  The marker isn't added to the map here.
 *
 * Parameters:
 * nMarkerType - {Integer} The marker type
 * oLatLng     - {<GLatLng>} A GLatLng object
 *
 * Returns:
 * {<GMarker>}
 */
RoutingControl.prototype.createMarker = function(nMarkerType, oLatLng) {
    var oMarker, oIcon, oMarkerOptions, nWidth, nHeight;
    oMarkerOptions = this.main.markers;

    oIcon = new GIcon(G_DEFAULT_ICON);

    // check for 'image' options
    if(oMarkerOptions[nMarkerType]['image']) {
        var oImage = oMarkerOptions[nMarkerType]['image'];
        if(oImage['image']) { 
            oIcon.image = oImage['image'];
        }
        if(oImage['width'] && oImage['height']) {
            oIcon.iconSize = new GSize(oImage['width'], oImage['height']);
        }
    }

    // check for 'shadow' options
    if(oMarkerOptions[nMarkerType]['shadow']) {
        var oShadow = oMarkerOptions[nMarkerType]['shadow'];
        if(oShadow['image']) { 
            oIcon.shadow = oShadow['image'];
        }
        if(oShadow['width'] && oShadow['height']) {
            oIcon.shadowSize = new GSize(oShadow['width'], oShadow['height']);
        }
    }

    oMarker = new GMarker(oLatLng,{draggable: true, icon:oIcon});
    oMarker.routing = this;
    oMarker.markerType = nMarkerType;

    return oMarker;
};

/**
 * Method: onInitMarkerDragStart
 * Called before an initMarker is dragged.  Deactivate the map "mousemove"
 *     event and tell the routing module it's currently 'dragging'.
 */
RoutingControl.prototype.onInitMarkerDragStart = function() {
    this.routing.draggingMarker = true;
    this.routing.main.removeMapMouseMoveListener();
};

/**
 * Method: onInitMarkerDragEnd
 * Called when an initMarker was dragged.  Reactivate the map "mousemove" event,
 *     hide the initMarker and create a marker at the dragged position.
 */
RoutingControl.prototype.onInitMarkerDragEnd = function() {
    var oRouting = this.routing;
    oRouting.draggingMarker = false;
    oRouting.main.addMapMouseMoveListener();
    this.hide();
    var oLatLng = this.getLatLng();    
    var oMarker = oRouting.createMarker(this.markerType, oLatLng);

    oRouting.addMarker(oMarker);

    var oBindedOptions = {
        'marker': oMarker,
        'main': this,
        'routing': oRouting
    };

    GEvent.bind(oMarker, "dragstart", oMarker, oRouting.onMarkerDragStart);
    GEvent.bind(
        oMarker,
        'dragend', 
        oBindedOptions,
        oRouting.getMarkerAdjustment
    );

    oRouting.getMarkerAdjustment(oBindedOptions);
};

/**
 * Method: getLatLngFromMarkerType
 * Given a marker, get the corresponding initMarker.  Show the initMarker and 
 *     reset its position.
 *
 * Parameters:
 * oMarker - {<GMarker>} The marker used to find the corresponding initMarker.
 */
RoutingControl.prototype.resetInitMarker = function(oMarker) {
    nMarkerType = (typeof(oMarker) == 'string') ? oMarker : oMarker.markerType;
    
    var oInitMarker = this.getMarkerByType(this.initMarkers, nMarkerType);
    var oInitLatLng = this.getLatLngFromMarkerType(oMarker.markerType);

    oInitMarker.show();
    oInitMarker.setLatLng(oInitLatLng);
};

/**
 * Method: addMarker
 * Add the given marker the map and to the markers array depending on its
 * 'markerType' property.
 *
 * Parameters:
 * oMarker - {<GMarker>} The marker to add
 */
RoutingControl.prototype.addMarker = function(oMarker) {
    var nMarkerType = oMarker.markerType;

    // add the marker to the map
    this.map.addOverlay(oMarker);

    // add the marker to the markers array depending on its type
    switch (nMarkerType) {
      case this.startMarkerType:
        this.markers.unshift(oMarker);
        break;
      case this.endMarkerType:
        this.markers.push(oMarker);
        break;
      case this.relayMarkerType:
        this.markers.splice(oMarker.initIndex, 0, oMarker);

        // when adding a new relay marker, insert an empty array to the 
        // routing overlays array.
        this.routingOverlays.splice(oMarker.initIndex-1, 0, []);

        // change the existing overlays relation to the markers accordingly
        this.changeOverlayIndexes(oMarker.initIndex-1, 1);

        delete oMarker.initIndex;
        break;
      default:
        alert("error, unsupported markerType : "+nMarkerType);
    }
};

/**
 * Method: removeMarker
 * Removes the given marker from the map and from the markers array.
 *
 * Parameters:
 * oMarker - {<GMarker>} The marker to remove
 */
RoutingControl.prototype.removeMarker = function(oMarker) {
    this.map.removeOverlay(oMarker);
    this.markers.splice(OpenLayers.Util.indexOf(this.markers, oMarker), 1);
};

/**
 * Method: onMapMove
 * Called when the map moves.  Binded to an 'initMarker'.  Moves the marker
 *     as the map moves to a specific position (in options.js).
 */
RoutingControl.prototype.onMapMove = function() {
    this.setLatLng(this.routing.getLatLngFromMarkerType(this.markerType));
};

/**
 * Method: getLatLngFromMarkerType
 * Given a marker type, get the corresponding offset x and y parameters from
 *     the 'main marker' options.  Return a GLatLng object from theses params.
 *
 * Parameters:
 * nMarkerType - {Integer} The marker type
 *
 * Returns:
 * {<GLatLng>}
 */
RoutingControl.prototype.getLatLngFromMarkerType = function(nMarkerType) {
    var oMarkerOptions, nOffsetX, nOffsetY, oPoint, oLatLng;
    oMarkerOptions = this.main.markers;

    nOffsetX =  oMarkerOptions[nMarkerType]['offsetx'];
    nOffsetY =  oMarkerOptions[nMarkerType]['offsety'];
    oPoint = new GPoint(nOffsetX,nOffsetY);

    oLatLng = this.map.fromContainerPixelToLatLng(oPoint);

    return oLatLng;
};

/**
 * Method: onSingleRightClick
 * Called when the map is clicked with the right button (once).  If the click
 *     was made on a marker, remove it.  If it was a 'start' or 'end' marker,
 *     reset the related 'initMarker' and set a new 'start' or 'end' marker
 *     from the existing ones.  If it was a 'relay' marker, remove it and
 *     manually recreate a getBestRoute request.
 *
 * Parameters:
 * oPoint   - {<GPoint>} A point on the my by its pixel coordinates.
 * oSrc     - {DOM} DOM element
 * oOverlay - {<GOverlay>} The marker clicked
 */
RoutingControl.prototype.onSingleRightClick = function(oPoint, oSrc, oOverlay){
    var oRouting, nMarkerType, nStartType, nEndType, nRelayType;

    oRouting = this.main.routing;

    var nIndex = OpenLayers.Util.indexOf(this.markers,oOverlay); 
    if (nIndex != -1) {
        var nMarkerType = oOverlay.markerType;
        var nStartType = oRouting.startMarkerType;
        var nEndType = oRouting.endMarkerType;
        var nRelayType = oRouting.relayMarkerType;

        switch (nMarkerType) {
            // "START" marker removed
          case nStartType:
            oRouting.removeMarker(oOverlay);
            oRouting.clearRoutingOverlaysByIndex(nIndex);
            oRouting.routingOverlays.shift();

            var oNewStartMarker = oRouting.markers[0];
            if(oNewStartMarker && oNewStartMarker.markerType == nRelayType) {
                var oStyle = this.main.getMarkerStyleObject(nStartType);
                oNewStartMarker.setImage(oStyle['image']);
                oNewStartMarker.markerType = nStartType;
                oRouting.changeOverlayIndexes(0,-1);
                oRouting.setDirections();
            } else {
                oRouting.resetInitMarker(oOverlay);
                oRouting.resetDirections();
            }

            break;

            // "END" marker removed
          case nEndType:
            oRouting.removeMarker(oOverlay);
            oRouting.clearRoutingOverlaysByIndex(nIndex-1);
            oRouting.routingOverlays.pop();

            var oNewEndMarker = oRouting.markers[oRouting.markers.length-1];
            if(oNewEndMarker && oNewEndMarker.markerType == nRelayType) {
                var oStyle = this.main.getMarkerStyleObject(nEndType);
                oNewEndMarker.setImage(oStyle['image']);
                oNewEndMarker.markerType = nEndType;
                oRouting.setDirections();
            } else {
                oRouting.resetInitMarker(oOverlay);
                oRouting.resetDirections();
            }
            break;

            // "RELAY" marker removed
          case nRelayType:
            oRouting.removeMarker(oOverlay);

            // clear overlays before and after the marker
            oRouting.clearRoutingOverlaysByIndex(nIndex);
            oRouting.clearRoutingOverlaysByIndex(nIndex-1);

            // remove one array of overlays (since only one is needed now
            oRouting.routingOverlays.splice(nIndex, 1);

            var oPreviousMarker = oRouting.markers[nIndex-1];
            oRouting.changeOverlayIndexes(nIndex,-1);

            // manually trigger a new "getBestRoute" request, as if the marker
            // that comes was moved
            oRouting.getBestRoute(oPreviousMarker, {'startOnly': true});

            break;
          default:
            alert("error: unsupported marker type "+oOverlay.markerType);
        }        

    }
    return;
};

/**
 * Method: resetMarkers
 * Remove all routing markers from the map.  When removing a 'start' or 'end'
 *     marker, restore its initMarker at the same time.  This method is used
 *     when 'reseting all' only, not one by one.
 */
RoutingControl.prototype.resetMarkers = function(){
    while(this.markers.length > 0) {
        var oMarker = this.markers[0];
        if(oMarker.markerType == this.startMarkerType ||
           oMarker.markerType == this.endMarkerType) {
            this.resetInitMarker(oMarker);
        }
        this.removeMarker(oMarker);
    }
};

/**
 * Method: getMarkerByType
 * Given a marker type, return the marker if it exists in a given markers array.
 *
 * Parameters:
 * aoMarkers   - {Array(<GMarker>)} Array of markers
 * nMarkerType - {Integer} The marker type
 *
 * Returns:
 * {<GMarker>} A marker corresponding to the given marker type.
 * featureId
 */
RoutingControl.prototype.getMarkerByType = function(aoMarkers, nMarkerType) {
    var oFoundMarker = false;
    for(var i=0; i<aoMarkers.length; i++) {
        var oMarker = aoMarkers[i];
        
        if(oMarker.markerType == nMarkerType) {
            oFoundMarker = oMarker;
            break;
        }
    }
    
    return oFoundMarker;
};

/**
 * Method: onMarkerDragStart
 * Called before a marker is dragged.  Deactivate the map "mousemove" event
 */
RoutingControl.prototype.onMarkerDragStart = function() {
    var oRouting = this.routing;

    oRouting.draggingMarker = true;
    oRouting.main.removeMapMouseMoveListener();

    if(this.markerType != oRouting.tempRelayMarkerType && 
       oRouting.tempRelayMarker) {
        oRouting.tempRelayMarker.hide();
    }
};

/**
 * Method: getMarkerAdjustment
 * Called when an initMarker OR marker is dragged to a location (point).
 *     Reactivate the map "mousemove" event and lunches a getPoint request to
 *     find the closest point of the closest tempOverlay on the map.  When the
 *     request is done, adjust the marker to the point returned.
 *
 * While waiting for the response of the request, the marker to adjust is kept
 *     in this.markerToAdjust.
 *
 * Parameters:
 * options - {Object} An object of options used to create the point.
 */
RoutingControl.prototype.getMarkerAdjustment = function(options) {
    var oMain = fcmq;
    var oRouting = this.routing || options.routing;
    var oMarker = this.marker || options.marker;

    oRouting.draggingMarker = false;

    oMain.addMapMouseMoveListener();

    if(oMarker.markerType != oRouting.tempRelayMarkerType && 
       oRouting.tempRelayMarker) {
        oRouting.tempRelayMarker.hide();
    }

    oRouting.markerToAdjust = oMarker;
    var szPoint = oMarker.getLatLng().lng()+","+oMarker.getLatLng().lat();

    var oOptions = {};
    if(oRouting.transQuebecOnly.checked) {
        oOptions['level'] = 1;
    }

    oMain.getClosestPoint(szPoint, oRouting.adjustMarker, oOptions);
};

/**
 * Method: adjustMarker
 * Called after a getPoint request.  Ajust this.markerToAdjust to the point
 *     returned by the request.  Calls the getBestRoute function right after the
 *     ajustment.
 *
 * Parameters:
 * doc - {AJAXResponse} The response from the AJAX query
 */
RoutingControl.prototype.adjustMarker = function(doc) {
    var oMain = fcmq;
    var oRouting = oMain.routing;
    var oMarker = oRouting.markerToAdjust;

    var aoOverlays = oMain.readGeoJSON(doc);
    
    if(aoOverlays.length == 0) {
        return;
    }

    var oOverlay = aoOverlays[0];
    oMarker.setLatLng(oOverlay.getLatLng());

    // hide pleaseWait
    oMain.hidePleaseWait();

    oRouting.getBestRoute(oMarker);
};

/**
 * Method: getBestRoute
 * Called after any routing marker was 'adjusted'.  If there is at least two
 *     markers on the map and the first and the last one are the 'start' and
 *     'end' marker, lunches a getRouting request.  On response, lunch the
 *    setBestRoute function.
 *
 * The method only lunches getRouting requests for the path between the marker
 *     located before and after oMarker.
 *
 * Parameters:
 * oMarker    - {<GOverlay>} The last marker that was moved around.
 * oOptions   - {Object} A hash of options for this request.
 */
RoutingControl.prototype.getBestRoute = function(oMarker, oOptions) {
    var oStartMarker, oEndMarker, oReqOptions, szStart, szEnd;

    oOptions = oOptions || {};

    // if there is less than 2 markers, no need to do anything...
    if(this.markers.length < 2) {
        return;
    }

    // if the first or last markers are not the real "start" or "end" marker,
    // no need to do anything
    if(this.markers[0].markerType != this.startMarkerType ||
       this.markers[this.markers.length-1].markerType != this.endMarkerType) {
        return;
    }

    nMarkerPos = OpenLayers.Util.indexOf(this.markers, oMarker);

    // if there is a marker before this one, send a request with the previous
    // marker as the "start" marker and this marker as the "end" marker
    if(oMarker.markerType != this.startMarkerType &&
       this.markers[nMarkerPos-1] && oOptions['startOnly'] !== true) {
        oReqOptions = {};
        if(this.transQuebecOnly.checked) {
            oReqOptions['level'] = 1;
        }
        oStartMarker = this.markers[nMarkerPos-1];
        szStart = oStartMarker.getLatLng().lng()+","+
                  oStartMarker.getLatLng().lat();
        oReqOptions[this.startIdProperty] = nMarkerPos-1;

        oEndMarker = oMarker;
        szEnd = oEndMarker.getLatLng().lng()+","+
                oEndMarker.getLatLng().lat();
        oReqOptions[this.endIdProperty] = nMarkerPos;

        // proceed with the request
        this.routingQueries++;
        this.main.getRouting(szStart, szEnd, this.setBestRoute, oReqOptions);
    }

    // if there is a marker before this one, send a request with the previous
    // marker as the "start" marker and this marker as the "end" marker
    if(oMarker.markerType != this.endMarkerType &&
       this.markers[nMarkerPos+1] && oOptions['endOnly'] !== true) {
        oReqOptions = {};
        if(this.transQuebecOnly.checked) {
            oReqOptions['level'] = 1;
        }
        oStartMarker = oMarker;
        szStart = oStartMarker.getLatLng().lng()+","+
                oStartMarker.getLatLng().lat();
        oReqOptions[this.startIdProperty] = nMarkerPos;

        oEndMarker = this.markers[nMarkerPos+1];
        szEnd = oEndMarker.getLatLng().lng()+","+
                  oEndMarker.getLatLng().lat();
        oReqOptions[this.endIdProperty] = nMarkerPos+1;

        // proceed with the request
        this.routingQueries++;
        this.main.getRouting(szStart, szEnd, this.setBestRoute, oReqOptions);
    }
};

/**
 * Method: setBestRoute
 * Called after a getRouting request response is received.  Read each overlays
 *     from the response.  For each overlay read, apply the 'routingStyle' to
 *     it.  Read the "lastId" property to know in which array of overlays these
 *     overlays need to be inserted into.
 *
 * Parameters:
 * doc - {AJAXResponse} The response from the AJAX query
 */
RoutingControl.prototype.setBestRoute = function(doc) {
    var oMain = fcmq;
    var oRouting = oMain.routing;
    var oOverlay, oRow, oBounds, bNewMarker;

    bNewMarker = ((oRouting.markers.length - 1) > oRouting.routingOverlays.length);

    // 1st: read the overlays
    var aoOverlays = oMain.readGeoJSON(doc);

    // 2nd: get the index of "where" to put the overlays
    var nIndex = aoOverlays[0].properties[oRouting.startIdProperty];

    // 3rd: clear previous routing overlays
    oRouting.clearRoutingOverlaysByIndex(nIndex);

    // 4th: add the new overlays to the right index and register events
    var aoNewOverlays = [];
    for(i=0; i<aoOverlays.length; i++) {
        oOverlay = aoOverlays[i];

        // add the overlay to the map and routingOverlays array
        oOverlay.setStrokeStyle(oMain.routingStyle);
        oMain.map.addOverlay(oOverlay);
        aoNewOverlays.push(oOverlay);

        // listen to mouseover and mouse out events
        GEvent.bind(oOverlay, 'mouseover', 
                    {'overlay': oOverlay,'main': fcmq, 'routing': oRouting},
                    oRouting.onOverlayMouseOver);
        GEvent.bind(oOverlay, 'mouseout',
                    {'overlay': oOverlay,'main': fcmq, 'routing': oRouting},
                    oRouting.onOverlayMouseOut);
    }

    oRouting.routingOverlays[nIndex] = aoNewOverlays;

    oRouting.routingQueries--;

    // last: when all routing queries were received, set the directions.
    if(oRouting.routingQueries === 0) {
        oRouting.sortRoutingOverlays();
        oRouting.setDirections();
    }
};

/**
 * Method: sortRoutingOverlays
 * Sort the routingOverlays arrays with the "startId" of the first overlay they
 *     have.
 */
RoutingControl.prototype.sortRoutingOverlays = function() {
    var aoSorted = [];

    for(var i=0; i<this.routingOverlays.length; i++) {
        var nIndex = parseInt(this.routingOverlays[i][0].properties[this.startIdProperty]);
        aoSorted[nIndex] = this.routingOverlays[i];
    }

    this.routingOverlays = aoSorted;
};

/**
 * Method: clearRoutingOverlaysByIndex
 * Remove overlays from a specific routingOverlay array from the map and from
 *     the array in question.  In the end, the array is left empty (it's not
 *     removed).
 *
 * Parameters:
 * nIndex - {Integer} The index of the routingOverlay array in question
 */
RoutingControl.prototype.clearRoutingOverlaysByIndex = function(nIndex) {
    var aoOverlays, oOverlay;

    aoOverlays = this.routingOverlays[nIndex];

    if(aoOverlays) {
        for(var i=0; i<aoOverlays.length; i++) {
            oOverlay = aoOverlays[i];
            this.map.removeOverlay(oOverlay);
        }
    }

    this.routingOverlays[nIndex] = [];
};

/**
 * Method: changeOverlayIndexes
 * Increment the 'startId' and 'endId' of existing routingOverlays from a 
 *     specific index and all those in the following indexes.  Used when 
 *     inserting a 'relay' marker and removing a 'start' or 'relay' marker.
 *
 * Parameters:
 * nIndex     - {Integer} The index of the routingOverlay array to start from.
 * nIncrement - {Integer} The value for incrementing the start and end ids 
 */
RoutingControl.prototype.changeOverlayIndexes = function(nIndex, nIncrement) {
    for(var i=nIndex; i<this.routingOverlays.length; i++) {
        for(var j=0; j<this.routingOverlays[i].length; j++) {
            var oOverlay = this.routingOverlays[i][j];

            oOverlay.properties[this.startIdProperty] =
                parseInt(oOverlay.properties[this.startIdProperty]) + nIncrement;
            oOverlay.properties[this.endIdProperty] =
                parseInt(oOverlay.properties[this.endIdProperty]) + nIncrement;
        }
    }
};

/**
 * Method: setDirections
 * Builds the direction html output from the current routingOverlays.
 */
RoutingControl.prototype.setDirections = function() {
    var oMain, oOverlay, nDir, aoOverlays, oBounds, oTable, oContainer,
        fTotalLength, fGrandTotalLength;

    oMain = this.main;
    nDir = 0;
    fGrandTotalLength = 0;
    oContainer = this.directionTableContainer;

    // 1st: destroy everything inside the old container
    this.resetDirections();

    // 2nd: for each routing category
    for(var i=0; i<this.routingOverlays.length; i++) {
        var fTotalLength = 0;
        aoOverlays = this.routingOverlays[i];

        // 1st: create a new table
        oTable = document.createElement("table");
        oTable.cellSpacing = 0;
        oTable.cellPadding = 2;
        oTable.className = this.directionTableClassName;

        // 2nd: "overlay" and "start" title row inserts
        this.insertOverlayTitleRow(oTable, i);
        this.insertMarkerTitleRow(oTable, i, true);

        // 3rd: insert a row for each overlay inside the category
        for(var j=0; j<aoOverlays.length; j++) {
            fTotalLength +=
                this.insertDirectionRow(oTable, aoOverlays[j], ++nDir);
        }

        // 4th: insert a "end" row
        this.insertMarkerTitleRow(oTable, i, false);

        // 5th: insert a "total" and "grand total" row
        fGrandTotalLength += fTotalLength;
        this.insertTotalRow(oTable, fTotalLength, false);
        this.insertTotalRow(oTable, fGrandTotalLength, true);

        // last: append the new table to the container
        oContainer.appendChild(oTable);
    }

    // automatically recenters the map
    if(this.getAutoRecenter()) {
        this.setCenterToRouting();
    }

    // show the direction div & hide pleaseWait
    this.hideDirectionDiv(false);
    oMain.hidePleaseWait();
};

/**
 * Method: clearRoutingOverlays
 * Remove all routing overlays from all indexes.
 */
RoutingControl.prototype.clearRoutingOverlays = function() {
    for(var i=0; i<this.routingOverlays.length; i++) {
        this.clearRoutingOverlaysByIndex(i);
    }
    this.routingOverlays = [];
};

/**
 * Method: resetAll
 * Reset the markers, routingOverlays and directions
 */
RoutingControl.prototype.resetAll = function() {
    this.resetMarkers();
    this.clearRoutingOverlays();
    this.resetDirections();
};

/**
 * Method: resetDirections
 * Clear the directions html from the container div and hide it.
 */
RoutingControl.prototype.resetDirections = function() {
    this.directionTableContainer.innerHTML = "";
    this.hideDirectionDiv(true);
};

/**
 * Method: hideDirectionDiv
 * Hides the direction div if bHide is set to true, else show it.
 *
 * Parameters:
 * bHide - {Boolean} Hide the div is set to true
 */
RoutingControl.prototype.hideDirectionDiv = function(bHide) {
    if(bHide) {
        this.directionDiv.style.display = "none";
    } else {
        this.directionDiv.style.display = "";
    }
};

/**
 * Method: insertOverlayTitleRow
 * Add a row to the direction table containing a single cell.  Its content is
 *     a title row for a single segment.
 *
 * Parameters:
 * oTable   - {DOM} The <table> DOM element.
 * nIndex   - {Integer} Used to set the direction number to display
 */
RoutingControl.prototype.insertOverlayTitleRow = function(oTable, nIndex) {
    var oLang = lang[this.main['lang']];
    var szText = oLang['dir_overlay']+(nIndex+1);

    // get marker lat
    var szOnClick = "(fcmq) ? fcmq.routing.setCenterToSegment("+nIndex+") : alert(1);";

    // insert row
    var nLastRow = oTable.rows.length;
    var oRow = oTable.insertRow(nLastRow);

    // single cell
    var cell = oRow.insertCell(0);
    cell.className = "dirOverlayTitle";
    cell.colSpan = 3;
    cell.onclick = new Function(szOnClick);
    cell.title = oLang['dir_location_overlay_title'];
    var textNode = document.createTextNode(szText);
    cell.appendChild(textNode);
};

/**
 * Method: insertMarkerTitleRow
 * Add a row to the direction table containing a single cell.  If bStart is
 *     set to 'true', then a 'start' row is added, else it's a 'end' row.
 *
 * Parameters:
 * oTable   - {DOM} The <table> DOM element.
 * nIndex   - {Integer} The direction number to display
 * bStart   - {Boolean} 'true' if adding a 'start' row, else 'false' for
 *                      a 'end' row.
 */
RoutingControl.prototype.insertMarkerTitleRow = function(oTable, nIndex, bStart) {
    var szText = "";
    var nMarker;
    var szLang = this.main['lang'];
    var oLang = lang[szLang];

    // if "Start" row
    if(bStart){
        nMarker = nIndex;
        szText += oLang['dir_start']
        // if first "Start" row
        if(nIndex == 0) {
            szText += oLang['dir_start_location'];
        } else {
            szText += oLang['dir_relay_location']+nIndex;
        }
    }
    // if "End" row
    else {
        nMarker = nIndex+1;
        szText += oLang['dir_end'];
        if (nIndex == (this.routingOverlays.length - 1)) {
            szText += oLang['dir_end_location'];
        } else {
            szText += oLang['dir_relay_location']+(nIndex+1);
        }
    }

    // get marker lat
    var szOnClick = "(fcmq) ? fcmq.routing.setCenterToMarker("+nMarker+") : alert(1);";

    // insert row
    var nLastRow = oTable.rows.length;
    var oRow = oTable.insertRow(nLastRow);

    // single cell
    var cell = oRow.insertCell(0);
    cell.className = "dirMarkerTitle";
    cell.colSpan = 3;
    cell.onclick = new Function(szOnClick);
    cell.title = oLang['dir_location_marker_title'];
    var textNode = document.createTextNode(szText);
    cell.appendChild(textNode);
};

/**
 * Method: insertDirectionRow
 * Given a specific oOverlay, add a row to the direction table.  Theses cells
 *     are added to the row :
 *     - direction number, from nNum
 *     - direction text (indications), from directionProperty of the oOverlay
 *     - length in km, from the lengthProperty of the oOverlay
 *
 * Parameters:
 * oTable   - {DOM} The <table> DOM element.
 * oOverlay - {<GOverlay>} The routing oOverlay in question
 * nNum     - {Integer} The direction number to display
 *
 * Returns:
 * {float} - Length in Kilometers of the oOverlay added.
 */
RoutingControl.prototype.insertDirectionRow = function(oTable, oOverlay, nNum) {
    var oDistCalc = this.distanceCalculator;
    var nLastRow = oTable.rows.length;
    var oRow = oTable.insertRow(nLastRow);
    oRow.className = "dirRow";
    oRow.className += (parseInt(nNum)%2) ? "" : " dirRowGray";

    // left cell
    var cellNum = oRow.insertCell(0);
    cellNum.className = "dirNum";
    var textNode = document.createTextNode(nNum+".");
    cellNum.appendChild(textNode);
  
    // center cell
    var cellTxt = oRow.insertCell(1);
    cellTxt.className = "dirTxt";

    var oDirNode;

    if(oOverlay.properties[this.angleProperty] && 
       oOverlay.properties[this.angleFactorProperty] &&
       oOverlay.properties[this.numberProperty] ) {
        var szAngle = oOverlay.properties[this.angleProperty];
        var szAngleFactor = oOverlay.properties[this.angleFactorProperty];
        var anNumber = oOverlay.properties[this.numberProperty];

        var fAngle = OpenLayers.Util.toFixedFloat(szAngle);
        var fAngleFactor = OpenLayers.Util.toFixedFloat(szAngleFactor);

        oDirNode = this.getDirectionNodeFromAngles(fAngle, fAngleFactor, anNumber);
    } else {
        var szDirection = "";
        var aDirectionTxt = oOverlay.properties[this.directionProperty];
        if (typeof(aDirectionTxt) == 'string') {
            szDirection = aDirectionTxt;
        } else {
            for(var i=0; i<aDirectionTxt.length; i++) {
                szDirection += (i>0)? "  ": "";
                szDirection += aDirectionTxt[i];
            }
        }
        oDirNode = document.createTextNode(szDirection);
    }

    cellTxt.appendChild(oDirNode);

    // right cell
    var cellKM = oRow.insertCell(2);
    cellKM.className = "dirKM";
    var fLength = oDistCalc.getOverlayLength(oOverlay);
    var fKM = oDistCalc.getKilometers(fLength);
    var textNode = document.createTextNode(fKM+" km");
    cellKM.appendChild(textNode);

    return fKM;
};

/**
 * Method: insertTotalRow
 * Add a row to the direction table containing a single cell :
 *     - a 'total' or 'grand total' in kilometers
 *
 * Parameters:
 * oTable      - {DOM} The <table> DOM element.
 * nLength     - {Float} The length in kilometers
 * bGrandTotal - {Boolean} 'true' to output 'grand total', false to output
 *                         'total'.
 */
RoutingControl.prototype.insertTotalRow = function(oTable, nLength, bGrandTotal) {
    var szLang = this.main['lang'];
    var oLang = lang[szLang];
    var oDistCalc = this.distanceCalculator;
    var nLastRow = oTable.rows.length;
    var oRow = oTable.insertRow(nLastRow);
    var nPrec = oDistCalc.lengthPrecision;
    var szTotal = (bGrandTotal) ? oLang['dir_grand_total'] : oLang['dir_total'];

    // single cell
    var cell = oRow.insertCell(0);
    cell.className = (bGrandTotal)? "dirGrandTotal" : "dirTotal";
    cell.colSpan = 3;
    nLength = OpenLayers.Util.toFixedFloat(nLength, nPrec);
    var textNode = document.createTextNode(szTotal+" : "+nLength+" km");
    cell.appendChild(textNode);
};

/**
 * Method: getDirectionNodeFromAngles
 * From two angle properties and an array of numbers, create a dom element
 *     containing the text and shield images of complete directions.  Depends
 *     on : cardinalDirections and turnDirections properties defined in
 *     options.js
 *
 * Parameters:
 * fAngle       - {Float} The angle of the line.  Used to get the cardinal
 *                        direction.
 * fAngleFactor - {Float} The difference of angle between the last line and this
 *                        this one.  If negative, that means we're turning left,
 *                        else right.  Also used to calculate how much wide we
 *                        turn.
 * anNumber     - {Array} Array of numbers used to get shield images.
 *
 * Returns:
 * {DOMElement} Div element filled with directions text and shield images
 */
RoutingControl.prototype.getDirectionNodeFromAngles = function(
    fAngle, fAngleFactor, anNumber) {
    
    var szLang = this.main['lang'];
    var oLang = lang[szLang];
    var oContainer = document.createElement("div");
    var aDirectionList = [], szDirections;

    // 1st : "continue" or "turn" or "wide turn" etc.
    var fPosAngleFactor = (fAngleFactor >= 0) ? fAngleFactor : fAngleFactor * -1;
    var szTurnValue;
    for(var i=0; i<this.turnDirectionsOrder.length; i++) {
        var nDir = this.turnDirectionsOrder[i];
        if(fPosAngleFactor < nDir) {
            szTurnValue = this.turnDirections[nDir];
            break;
        }
    }
    aDirectionList.push(oLang['dir_t_'+szTurnValue]);

    // 2nd : "left" or "right" if not "continue"
    if(szTurnValue != 'c') {
        aDirectionList.push((fAngleFactor > 0) ? oLang['dir_r'] : oLang['dir_l']);
    }

    // 3rd : if there's any number, from current direction list : create text
    //       node, add it to container, then add the shields nodes and finally
    //       go on with a new blank direction list.
    if(anNumber && anNumber.length > 0 && anNumber[0] != "") {
        aDirectionList.push(oLang['dir_on']+" ");
        oTextNode = document.createTextNode(aDirectionList.join(" "));
        oContainer.appendChild(oTextNode);
        this.main.appendShieldsToElement(anNumber, oContainer);
        aDirectionList = [];
        aDirectionList.push("");
    }

    // 4th : "towards"
    aDirectionList.push(oLang['dir_dir']);

    // 5th : "north" or "south" or etc.
    var szCardDir;
    for(var i=0; i<this.cardinalDirectionsOrder.length; i++) {
        var nDir = this.cardinalDirectionsOrder[i];
        if(fAngle < nDir) {
            szCardDir = this.cardinalDirections[nDir];
            break;
        }
    }
    aDirectionList.push(oLang['dir_c_'+szCardDir]);

    // Last : create text node with remaining direction list and add it to
    //        the container and return it
    oTextNode = document.createTextNode(aDirectionList.join(" "));
    oContainer.appendChild(oTextNode);

    return oContainer;
};

/**
 * Method: addMapMouseMoveListener
 * Adds a "mousemove" listener binded to the map.
 */
RoutingControl.prototype.addMapMouseMoveListener = function() {
    if(!this.mapMouseMove) {
        this.mapMouseMove = GEvent.addListener(this.map, "mousemove",
                                               this.onMapMouseMove);
    }
};

/**
 * Method: removeMapMouseMoveListener
 * Removes the "mousemove" listener currently binded to the map.
 */
RoutingControl.prototype.removeMapMouseMoveListener = function() {
    if(this.mapMouseMove) {
        GEvent.removeListener(this.mapMouseMove);
        this.mapMouseMove = null;
    }
};

/**
 * Method: onMapMouseMove
 * Called when the mouse moves over the map.  If no "tempRelayMarker" has been
 * created yet, create one and put it at the current location.  If it's
 * existant, simply move it to oLatLng location and show it.
 * 
 * Parameters:
 * oLatLng   - {<GLatLng>} The coordnates of the mouse
 */
RoutingControl.prototype.onMapMouseMove = function(oLatLng) {
    var oMain = this.main;
    var oRouting = this.routing;
    var oMarker = oRouting.tempRelayMarker;

    // test
    if(!oMarker) {
        var oStyle = oMain.getMarkerStyleObject(oRouting.tempRelayMarkerType, null, {'iconAnchor': "CC"});
        var oIcon = new GIcon(oStyle);

        oMarker = new GMarker(oLatLng, {draggable: true, icon: oIcon});
        GEvent.bind(oMarker, "dragstart", oMarker, oRouting.onTempRelayMarkerDragStart);
        GEvent.bind(oMarker, "dragend", oMarker, oRouting.onTempRelayMarkerDragEnd);
        this.addOverlay(oMarker);
        oRouting.tempRelayMarker = oMarker;
        oMarker.routing = oRouting;
        oMarker.markerType = oRouting.tempRelayMarkerType;
    } else {
        oMarker.show();
        oMarker.setLatLng(oLatLng);
    }

};

/**
 * Method: onOverlayMouseOver
 * Called when the mouse is over a routing overlay (line).  Register a map 
 * "mousemove" event.
 */
RoutingControl.prototype.onOverlayMouseOver = function() {
    var oMain = this.main;
    var oOverlay = this.overlay;
    var oRouting = this.routing;

    oRouting.addMapMouseMoveListener();

    // if the user isn't currently dragging any marker, keep track of the
    // relayIndex to use next from current overlay hovered.
    if(!oRouting.draggingMarker) {
        oRouting.relayIndex = oOverlay.properties[oRouting.endIdProperty];
    }

    if(oMain.debug) {
        document.getElementById("myOverLineRouting").innerHTML = "";
        document.getElementById("myOverLineRouting").innerHTML = "in";
    }
};

/**
 * Method: onOverlayMouseOut
 * Called when the mouse gets out of a routing overlay (line).  Remove any
 * "mousemove" event created.
 */
RoutingControl.prototype.onOverlayMouseOut = function() {
    var oMain = this.main;
    var oOverlay = this.overlay;
    var oRouting = this.routing;

    if(oRouting.tempRelayMarker && oRouting.draggingMarker === false) {
        oRouting.tempRelayMarker.hide();
    }

    oRouting.removeMapMouseMoveListener();

    if(oMain.debug) {
        document.getElementById("myOverLineRouting").innerHTML = "";
        document.getElementById("myOverLineRouting").innerHTML = "out";
    }
};

/**
 * Method: onTempRelayMarkerDragStart
 * Called when a 'tempRelay' marker is dragged.  Tell the routing control it's
 *     currently dragging something and remove the map "mousemove" event.
 */
RoutingControl.prototype.onTempRelayMarkerDragStart = function() {
    this.routing.draggingMarker = true;
    this.routing.removeMapMouseMoveListener();
};

/**
 * Method: onTempRelayMarkerDragEnd
 * Called when a 'tempRelay' marker has been dragged.  Tell the routing
 *     control it's not dragging anymore.  Then, create and add a new 'relay'
 *     marker at the dragged location, binding some events at the same time.
 *
 * Call "getMarkerAdjustment" after the marker was added.
 */
RoutingControl.prototype.onTempRelayMarkerDragEnd = function() {
    var oRouting = this.routing;
    var nMarkerType = oRouting.relayMarkerType;

    oRouting.draggingMarker = false;
    this.hide();
    var oLatLng = this.getLatLng();    
    var oMarker = oRouting.createMarker(nMarkerType, oLatLng);
    oMarker.initIndex = oRouting.relayIndex;

    oRouting.addMarker(oMarker);

    var oBindedOptions = {
        'marker': oMarker,
        'main': this,
        'routing': oRouting
    };

    GEvent.bind(oMarker, "dragstart", oMarker, oRouting.onMarkerDragStart);
    GEvent.bind(
        oMarker,
        'dragend', 
        oBindedOptions,
        oRouting.getMarkerAdjustment
    );

    oRouting.getMarkerAdjustment(oBindedOptions);
};

/**
 * Method: setCenterToMarker
 * Called when the user clicks the "start" or "end" title in the routing 
 *     direction DOM.  From the index passed (nMarker), move the map to the
 *     corresponding marker location.
 *
 * nMarker   - {Integer} The index (inside this.markers array) of the marker
 *                       to move the map to.
 */
RoutingControl.prototype.setCenterToMarker = function(nMarker) {
    var oMarker = this.markers[nMarker];
    this.map.setCenter(oMarker.getLatLng());
};

/**
 * Method: setCenterToSegment
 * Called when the user clicks an "overlay" title row in the routing 
 *     direction DOM.  From the index passed (nIndex), move the map to the
 *     corresponding overlays BBOX.
 *
 * nIndex   - {Integer} The index (inside this.routingOverlays) of the overlays
 *                      to move the map to.
 */
RoutingControl.prototype.setCenterToSegment = function(nIndex) {
    var aoOverlays = this.routingOverlays[nIndex];
    var oBounds = this.getSegmentBounds(aoOverlays, {'ratio': true});
    this.map.setCenter(oBounds.getCenter(), 
                       this.map.getBoundsZoomLevel(oBounds));
};

/**
 * Method: setCenterToRouting
 * Called when the user clicks the routing title div.  Get the bounds of the
 *     whole routing overlays and recenters the map to it.
 */
RoutingControl.prototype.setCenterToRouting = function() {
    var oRoutingBounds = this.getRoutingBounds();
    this.map.setCenter(oRoutingBounds.getCenter(), 
                       this.map.getBoundsZoomLevel(oRoutingBounds));
};

/**
 * Method: getSegmentBounds
 * Calculate the BoundingBox of a routing segment (overlays between two 
 *     markers).
 *
 * Parameters:
 * aoOverlays - {Array(<GOverlay>)} Array of overlays making a segment.
 * oOptions   - {Object} A hash of options.
 *
 * Returns:
 * {<GLatLngBounds>}
 */
RoutingControl.prototype.getSegmentBounds = function(aoOverlays, oOptions) {
    oOptions = oOptions || {};
    var oSegmentBounds = new GLatLngBounds();

    for(var i=0; i<aoOverlays.length; i++) {
        var oBounds = aoOverlays[i].getBounds();
        oSegmentBounds.extend(oBounds.getSouthWest());
        oSegmentBounds.extend(oBounds.getNorthEast());
    }
    
    if(this.routingRatio && oOptions['ratio'] === true) {
        oSegmentBounds = oSegmentBounds.getBoundsFromRatio(this.routingRatio);
    }

    return oSegmentBounds;
};

/**
 * Method: getRoutingBounds
 * Calculate the whole BoundingBox of the routing overlays.  Extend the bound
 *     to a specific ratio if defined.
 *
 * Returns:
 * {<GLatLngBounds>}
 */
RoutingControl.prototype.getRoutingBounds = function() {
    var oRoutingBounds = new GLatLngBounds();

    for(var i=0; i<this.routingOverlays.length; i++) {
        var oSegmentBounds = this.getSegmentBounds(this.routingOverlays[i]);
        oRoutingBounds.extend(oSegmentBounds.getSouthWest());
        oRoutingBounds.extend(oSegmentBounds.getNorthEast());
    }
    
    if(this.routingRatio) {
        oRoutingBounds = oRoutingBounds.getBoundsFromRatio(this.routingRatio);
    }

    return oRoutingBounds;
};

/**
 * Method: getAutoRecenter
 * Returns if the 'autoRecenter' checkbox is currently checked.
 *
 * Returns:
 * {Boolean}
 */
RoutingControl.prototype.getAutoRecenter = function() {
    return this.autoRecenter.checked;
};

/**
 * Method: initText
 * Called only once after the control was created.  Initialize some text and
 *     images.
 */
RoutingControl.prototype.initText = function() {
    var oLang = lang[this.main['lang']];

    this.autoRecenter.title = oLang['routingAutoRecenterTitle'];
    this.directionTitleDiv.title = oLang['dir_location_routing_title']
};

