//  Better Live departure Boards
//  ============================
//
//  Explanation
//  -----------
//  The format for the National Rail Enquiries LDBs shows lack of imagination and thought.
//  It doesn't take much to see how it could be improved to transfer the facility of being 
//  able to see arrivals and departures from a VDU on a platform to the web without getting
//  stuck with the idiom.  In short *use* the facilities of the web for a better user-experience.
//
//  I live in Witham, Essex and often wish to travel to Colchester (In the "Down" direction.) but
//  when I look at LDB I'm confrontend with (a) lots of departures going the other way which are
//  essentially useless and (b) some departures to the Braintree branch which don't go to Colchester.
//  Wouldn't it be nice to filter-out these unwanted departures.  Anyway, that's how it started...
//  ...The result here is a bit more spohisticated as a result of re-thinking the uses of LDBs
//  from scratch.  
//    •  Departures are listed by terminal station not time 
//    •  All the departures for a given terminal are listed together on one line.  This makes
//       the display much more compact and easier to scan.
//    •  The current status of a train is more clearly shown using colour and other graphics
//    •  The time the train will arrive/depart is clearer as is the lateness.
//    •  When a train 'fails to report', often a sign of disruption, (and where the LDB 
//       times are now guesswork) this is more clearly highlighted.
//  Plus
//    •  Stations can be simply grouped to put all in one direction together.  (In my case 
//       I have 'U' for "Up" ie. those going to London, 'B' for the branch to Braintree and
//       'D' for "Down", ie. Marks Tey, Colchester and beyond.)
//    •  Preferred station groupings are automatically remembered. 
//    •  An indication is shown if trains appear to be running out of sequence.  For example
//       if the 10:10 is delayed by 30 minutes it may now leave after the 10:30(on-time)
//
//  More details at http://vulpeculox.net/rail
//
//
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
// Updating procedure
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 
// 1 When testing in GM is finished
//   a  Update version history below
//   b  save copy to local ...\vulpeculox\rail
// 2 Go to the user script compiler at http://arantius.com/misc/greasemonkey/script-compiler
//     GUID           : 505c8cdf-5c77-4983-a986-65c023212a5c
//     Name           : Peter Fox       
//     Extension home : http://vulpeculox.net/rail
//     Version        : <version number>  eg 1.0.7
//     Script         : Cut and paste .js file
//   and create an xpi file download 
// 3 Move downloaded .xpi file to ...\vulpeculox\rail
// 4 Generate a SHA1 hash (Programme Files\Tools\HashGen\dpsha.exe)
// 5 Edit program files\vulpeculox\rail\index.htm 
//   a  Change the hash at about line 35 with the result of 4
//      (Don't o/w the sha1: at the start!)
//   b  Change the version date about line 73
// 6 FTP betterldb.xpi and index.htm  and version.htm to live server
// 7 Run FF,Tools,Add-ons,find updates or go to home and add
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 

// ==UserScript==
// @name           BetterLDB
// @namespace      http://vulpeculox.net
// @description    Better live deprture boards
// @include        http://realtime.nationalrail.co.uk/ldb/sum*
// @include        http://vulpeculox.net/rail*
// ==/UserScript==


window.versionDate   = '26 Dec 2009';
window.versionNumber = '1.0.6'; 
// Version history 
// ---------------
// 26 Dec 2009   First release


// --------------------------------------------------------
function GetAllMatchingElements(TagAndClass,StartFrom){
// Encapsulates document.evaluate() in original order
// TagAndClass eg   "//td [@class='date focus today']"
// StartFrom is typically document but can be any element
// --------------------------------------------------------
  return document.evaluate(TagAndClass,StartFrom,null,XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,null);
}

// --------------------------------------------------------
function GetFirstMatchingElement(TagAndClass,StartFrom){
// Encapsulates document.evaluate()...first element
// TagAndClass eg   "//td [@class='date focus today']"
// StartFrom is typically document but can be any element
// --------------------------------------------------------
  //LogElement('GFMA ('+TagAndClass+') ',StartFrom);
  var matchingElements = GetAllMatchingElements(TagAndClass,StartFrom);
  //GM_log('NUMBER FOUND : ' + matchingElements.snapshotLength);
  if (matchingElements.snapshotLength>0){
    for(var i = 0; i<matchingElements.snapshotLength;i++){
      //LogElement('ELMT('+i+') :',matchingElements.snapshotItem(i));
    }    
    return matchingElements.snapshotItem(0);  
  }else{
    return undefined;
  }
}



// --------------------------------------------------------
function ApplyToMatchingElements(TagAndClass,StartFrom,Callback){
// apply some function to the discovered elements
// --------------------------------------------------------
  var xpr = GetAllMatchingElements(TagAndClass,StartFrom);
  if(xpr.snapshotLength<1){return;}
  for (var i = 0; i < xpr.snapshotLength; i++){
    Callback(xpr.snapshotItem(i));
  }
}

// --------------------------------------------------------
function AddStyle(Css) {
// Convenience function to add style
// Css is the text
// --------------------------------------------------------
  var head = document.getElementsByTagName('head')[0];
  if (!head) { return; }
  var style = document.createElement('style');
  style.type = 'text/css';
  style.innerHTML = Css;
  head.appendChild(style);
}

// --------------------------------------------------------
function LogElement(Text,Element){ 
// --------------------------------------------------------
  if(Element === undefined){
    GM_log(Text + ': Element is undefined');
  }else{  
    GM_log(Text + ': '+ Element.localName + ':' + Element.className + '{'+ Element.id+'}');
  }  
}

// --------------------------------------------------------
window.LogArray = function(Label,AnArray){
// Display an array in the log messages
// --------------------------------------------------------
  var s = AnArray.join('|');
  GM_log(Label + ' Array of ('+AnArray.length+') items: '+s);
};  

// --------------------------------------------------------
window.LogArray2 = function(Label,AnArray){
// Display a 2-dimensional array in the log messages
// --------------------------------------------------------
  var lines = new Array(AnArray.length);
  for(var i=0 ; i < AnArray.length ; i++){
    lines[i] = AnArray[i].join('|');
  }
  var s = lines.join('\n');
  GM_log(Label + ' Array of ('+AnArray.length+') rows:\n'+s);
};  


// -------------------------------------------------
window.SaveTwoDimArray = function(Key,AnArray){
// Generic 2 dim array to GM_setValue()
// -------------------------------------------------
  var lines = new Array(AnArray.length);
  //GM_log('S2DA:('+AnArray.length+')('+Key+')');
  for(var i=0 ; i < AnArray.length ; i++){
    lines[i] = AnArray[i].join('¬');
  }
  var s = lines.join('\n');
  //GM_log('S2DA:('+AnArray.length+')('+s+')');
  GM_setValue(Key,s);
}

// -------------------------------------------------
window.LoadTwoDimArray = function(Key,DefaultStr){
// Generic load a two dimensional array using GM_getValue()
// -------------------------------------------------
  var s = GM_getValue(Key,DefaultStr);
  //GM_log('Sis('+s+')');
  if(s==''){
    //GM_log('L2D ('+Key+')is blank');
    return new Array();
  }else{
    var lines = s.split('\n');
    var noLines = lines.length;
    //GM_log('L2D : '+noLines+' lines');
    var a = new Array();
    for(var i=0 ; i < noLines ; i++){
      //GM_log('L2D Line : '+i+' = ('+lines[i]+')');
      if(lines[i].indexOf('¬')>-1){
        a.push(lines[i].split('¬'));
      }  
    }
    return a;
  }  
}

//**************************************************************
//**************************************************************
//**************************************************************
//**************************************************************
//**************************************************************

// ----------------------------------------
function Button(Caption,URL){
// Link in the style of a button
// ----------------------------------------
  return '<a class=navBut1 href="'+URL+'">'+Caption+'</a>';
}  

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// ADD FOCUS AND CHANGED EVENTS TO GROUP FIELDS
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

// ----------------------------------------
window.HandlePopupMouseOver = function(event){
// ----------------------------------------
  // find the text content hidden in the span following the <A>
  var a = event.target;   // <a>
  if(a.tagName!='A'){a = a.parentNode;}  // over the span inside the A
  var s = a.nextSibling;  // <span>  
  var allInfo = s.textContent;    // this looks like foo(bar)buz where parts might be missing
  //GM_log(allInfo);
  var op='';
  var clock='';
  var togo='';
  if(allInfo !=''){    // hide/ignore for nothing
    var ob = allInfo.indexOf('(');
    var cb = allInfo.indexOf(')');
    if(cb==-1){
      op = allInfo;
    }else{
      op = allInfo.slice(cb+1);
      togo = allInfo.slice(ob+1,cb);
      if(ob>1){
        clock = allInfo.slice(0,ob);
      }
    }      
    // now fill
    if(clock != ''){
      window.moreInfo.childNodes[0].textContent=clock;
      window.moreInfo.childNodes[0].style.visibility = 'visible';
    }else{  
      window.moreInfo.childNodes[0].style.visibility = 'hidden';
    }
    if(togo != ''){
      window.moreInfo.childNodes[1].textContent=togo;
      window.moreInfo.childNodes[1].style.visibility = 'visible';
    }else{  
      window.moreInfo.childNodes[1].style.visibility = 'hidden';
    }
      
      
      
    window.moreInfo.childNodes[2].textContent=op;
  }  
  window.moreInfo.style.visibility = 'visible';
  
};
  
// ----------------------------------------
window.HandlePopupMouseOut = function(event){
// ----------------------------------------
  window.moreInfo.visibility = 'none';
};


// ----------------------------------------
window.HandleGroupMouseOver = function(event){
// ----------------------------------------
  var g = event.target;  // group element
  g.focus();
  g.style.border = '1px solid black';
  g.style.backgroundColor = 'white';
  // pop-up help TODO
};

// ----------------------------------------
window.HandleGroupMouseOut = function(event){
// ----------------------------------------
  var g = event.target;  // group element
  g.style.border = '1px solid white';
  g.style.backgroundColor = '#fc8';
  g.blur();
};

// ----------------------------------------
window.HandleGroupKeyPress = function(event){
// ----------------------------------------
  event.stopPropagation();
  var k = String.fromCharCode(event.charCode);
  var c = k.toUpperCase();
  if((c>='A')&&(c<='Z')){  // only interested in alpha
    var g = event.target;  // group element
    g.value = '';//c;           // update the display immediately
    var t = g.nextSibling.textContent; // terminus name
    for(var i=0;i<window.terminals.length;i++){
      if(window.terminals[i].indexOf(t)==2){
        window.terminals[i] = c + '|'+t;
        //alert(window.terminals[i]);
        break;
      }
    }
    window.SaveTerminusNames();
    window.terminals.sort();
  
    var html = window.BuildNewTable();
    window.newElement.innerHTML=html;
    window.InstallEvents();

  }  
  event.stopPropagation();
};

window.InstallEvents = function(){
  var groupFields = window.document.evaluate("//input[@class='code']",newElement,null,XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,null);
  for (var i = 0; i < groupFields.snapshotLength; i++){
    groupFields.snapshotItem(i).addEventListener('mouseover',window.HandleGroupMouseOver,true);
    groupFields.snapshotItem(i).addEventListener('mouseout',window.HandleGroupMouseOut,true);
    groupFields.snapshotItem(i).addEventListener('keypress',window.HandleGroupKeyPress,true);
  }
  var popups = window.document.evaluate("//span[@class='hiddenpu']",newElement,null,XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,null);
  for (i = 0; i < popups.snapshotLength; i++){
    var a = popups.snapshotItem(i).previousSibling;
    a.addEventListener('mouseover',window.HandlePopupMouseOver,true);
    a.addEventListener('mouseout',window.HandlePopupMouseOut,true);
  }
};

//---------------------------------------------
function ComputeAcronym(FullName){
// return the capitalised letters in the text
//---------------------------------------------
  var c;
  var rv = '';
  for(var i=0;i<FullName.length;i++){
    c = FullName.slice(i,i+1);
    if((c>='A')&&(c<='Z')){rv+=c;}
  }
  return rv;
}
  

//==================================================================================
function trainTime(terminus,hhmmMinutes,hhmm,expectedMinutes,trainDetailURL,actual,lost,lateness,info,opAcronym){
// Object used to encapsulate all we know about an arr/dep
// see below for property roster
//==================================================================================
  this.terminus           = terminus;        // origin or destination string
  this.hhmmMinutes        = hhmmMinutes;     // minutes past midnight (ish)
  this.hhmm               = hhmm;            // text as appears in scheduled time
  this.expectedMinutes    = expectedMinutes; // minutes as 'expected'  -1 for n/a
  this.trainDetailURL     = trainDetailURL;  // href to schedule for this train
  this.actual             = actual;    // boolean false if no reports or starts from here
                                       //         true if we have some live data/info
  this.lost               = lost;      // boolean "*" in expected = report gone awol 
  this.lateness           = lateness;  // {early,onTime,bit,late,very}   Style
  this.info               = info;      // +delay or abbreviated message
  this.unsequenced        = false;     // boolean true if a later train will now run earlier
  this.unsequencedG       = false;     // boolean true if a later train *IN GROUP* will now run earlier
  this.opAcronym          = opAcronym; // operator's acronym
  //------------------------------------------------------
  this.Expected = function(){
  // This returns the best known time or -1 if n/a
  //------------------------------------------------------
    var em = this.expectedMinutes;
    if((em<0)&&(this.lost===false)){em = hhmmMinutes;}
    return em;
  };  
  
  //------------------------------------------------------
  this.Group = function(){
  // What group do this terminus belong to?
  // (look up in window.terminals)
  //------------------------------------------------------
    var wt;
    var rv='?';
    for(var i=0;i<window.terminals.length;i++){
      wt = window.terminals[i];
      if(wt.slice(2)==this.terminus){
        rv = wt.slice(0,1);
        break;
      }
    }
    return rv;
  };  
  
}  

//**************************************************************
//**************************************************************
//**************************************************************
//**************************************************************
//**************************************************************


// -------------------------------------------------------------
function ScrapeServiceBulletin(Div){
// This is a service bulletin.  We tidy up the text and put
// into a persistent array of strings
// -------------------------------------------------------------
  var t = Div.textContent;         // get all the text
  var i = t.indexOf(' More');      // lose the "More details..." bit
  if(i>-1){t = t.slice(0,i);}
  window.serviceBulletins.push(t);
  // now save the URL for more info  (Only 1 in case of many)
  if(Div.childNodes.length > 1){
    window.servBullURL = Div.childNodes[1].href;
  }  
}  


// -------------------------------------------------------------
function ConvertRowToData(Row){
// We are given a row of raw data
// from this build a traintime data object and add it to the
// list of traintimes
// Row.childNodes
//   [1].textContent ....terminus
//   [5].textContent ....hhmm
//   [7].textContent ....expected
//   [11].childNodes[0].href ....trainDetailURL
// -------------------------------------------------------------
  // steal the raw data
  if(Row.childNodes.length<2){return;}
  var terminus = Row.childNodes[1].textContent;
  
  // special case for bus departures following.  This a flag set once
  // which will happen as we get to the beginning of buses
  if((terminus == 'DESTINATION')||(terminus == 'ORIGIN')){return;}
  if(terminus.indexOf('Bus ')===0){
    window.bustitution = true;
    return;
  }
  
  
  // check this is a genuine row
  // sometimes the platform is left out
  var colAdjust = 0;  // assume platform given
  var childCount = Row.childNodes.length;
  if(childCount < 11){return;}
  if(childCount == 11){colAdjust = 2;}
  
  var hhmm     = Row.childNodes[5 - colAdjust].textContent;
  var expected = Row.childNodes[7 - colAdjust].textContent;
  var operator = Row.childNodes[9 - colAdjust].textContent;
  var opAcronym = ComputeAcronym(operator);
  var trainDetailURL = Row.childNodes[11 - colAdjust].childNodes[0].href;
  // convert raw data into more useful stuff
  var lost = false;
  var hhmmMinutes = ConvertHHMMtoMins(hhmm);

  // possibly lost
  if(expected.indexOf('*') != -1){
    lost = true;
    expected = expected.slice(0,-1);
  }    
  // expected
  var expectedMinutes = ConvertHHMMtoMins(expected);  // returns -1 if not hh:mm
  var info = '????';
  var lateness= 'onTime';
  var actual = true;
  if(expectedMinutes >= 0){
    var delay = expectedMinutes - hhmmMinutes;
    // adjust if odd circumstances of fiddling with hours over midningt/4am has upset us
    if(delay > 1420){delay = delay - 1440;}    // more than a day late!
    if(delay < -30){delay = delay + 1440;}     // more than 30 mins early!
    if(delay===0){
      lateness= 'onTime';
      info='';
    }else{
      if(delay>0){info = '+' + String(delay);}else{ info = String(delay);}
      lateness='late';
      if(delay<0){
        lateness='early';
      }else{
        if(delay<15){lateness='bit';}
        if(delay>59){lateness='very';}
      }
      if(lost){
        lateness='lost';
        info= info + ' LOST';
      }
    }  
  }else{  // the expected column contains some text
    if(expected=='On time'){
      lateness='onTime';
      info='';
    }
    if(lost){
      lateness='lost';
      info='LOST';
    }else{
      if((expected=='No report')||(expected=='Starts here')){
        info='';
        actual=false;
      }
      if(expected=='Cancelled'){
        lateness='very';
        info='CANX';
      }
      if(expected=='Delayed'){
        lateness='very';
        info='??:??';
      }
    }
  }
  if(window.bustitution===true){
    info = 'BUS';
    lateness = 'bus';
  }
  // now add this onto the array of train times
  var tt = new trainTime(terminus,hhmmMinutes,hhmm,expectedMinutes,trainDetailURL,actual,lost,lateness,info,opAcronym);
  window.trainTimes.push(tt);
  //GM_log(tt.hhmm + ' | ' + tt.Expected());
  
}
  
  
  
  
  
//**************************************************************
//**************************************************************
//**************************************************************
//**************************************************************
//**************************************************************

// -------------------------------------------------------------
function ConvertHHMMtoMins(HhmmString){
// Convert a string of hh:mm format into number of minutes
// past midnight.  Allow for up to 4am to be of the previous day.
// If the string is not in hh:mm format then return -1
// -------------------------------------------------------------
  var rv = -1;
  if(2 == HhmmString.indexOf(':')){  // check there is a colon in the right place
    if(HhmmString.length==5){
      var hh = Number(HhmmString.slice(0,2));
      var mm = Number(HhmmString.slice(3));
      if(hh < 4){hh = hh+24;}
      rv = (60 * hh) + mm;
    }
  }
  return rv;
}


//----------------------------------------------------
function GetTerminusNames(){
// Return array of strings in to be displayed order
// Load from local config then add any found on the screen
//
// return array in window.terminals;

//
// Each terminal name consists of two parts a group letter
// and a name split by a bar  eg X|Braintree
//----------------------------------------------------

  var tt,actualTermName,termSearch;
  var terminalsStr='';
  terminalsStr = GM_getValue(window.stationCode,terminalsStr);
  
  // terminal str is now '' or 'A|name$A|Name$...  $'
  
  for(var i=0;i<window.trainTimes.length;i++){
    tt = window.trainTimes[i];  // trainTime object
    actualTermName =  tt.terminus;
    actualTermCode =  actualTermName.slice(0,1);
    termSearch = '|'+actualTermName+'$';
    if(terminalsStr.indexOf(termSearch)==-1){
      terminalsStr = terminalsStr + actualTermCode+ '|' + actualTermName + '$';
    }
  }
  var terminalsArray = terminalsStr.split('$');      // convert string to array
  terminalsArray.pop();                              // lose the null on the end
  terminalsArray.sort();
  window.terminals = terminalsArray;
  window.SaveTerminusNames();      // save persistently
}

//----------------------------------------------------
function LoadPreviousStations(){
// Load window.PreviousStations from local persistent store
//----------------------------------------------------
  window.PreviousStations = window.LoadTwoDimArray('PreviousLDB','');
  //GM_log('loadPS');
}
  
//----------------------------------------------------
function SavePreviousStations(){
// Save the array of previous stations
//----------------------------------------------------
  //GM_log('savePS='+window.PreviousStations);
  return window.SaveTwoDimArray('PreviousLDB',window.PreviousStations);
}

//----------------------------------------------------
function AddStationToPrevious(Name,URL){
// Add the name and URL of the current LDB to the 
// persistent list
//----------------------------------------------------
  var psn = '';
  var tempArray = new Array(new Array(Name,URL));
  for(var i =0;i<window.PreviousStations.length;i++){
    psn = window.PreviousStations[i][0];
    if(Name != psn){
      //GM_log('('+Name + ') | (' + psn+')');
      tempArray.push(window.PreviousStations[i]);
    }
  }    
  // the slice below trims the length to some maximum
  window.PreviousStations = tempArray.slice(0,7);
}

//----------------------------------------------------
function BuildPreviousStationButtons(ThisStationName){
//----------------------------------------------------
  var buttons = '';
  for(var i =0;i<window.PreviousStations.length;i++){
    if(ThisStationName!=window.PreviousStations[i][0]){
      //GM_log(window.PreviousStations[i][0]);
      buttons += Button(window.PreviousStations[i][0],window.PreviousStations[i][1]);
    }  
  }  
  return buttons;
}

//----------------------------------------------------
window.SaveTerminusNames = function(){
// Stick the list of terminal names onto local
// persistent store
//----------------------------------------------------
  var taStr = window.terminals.join('$') + '$';
  GM_setValue(window.stationCode,taStr);
};

//----------------------------------------------------
window.BuildTermDataStructure = function (){
// we have two 'inputs' (a) the list of terminal names
// in the order we want them displayed and (b) the
// (global) array of trainTime objects.
// We use this to produce an array of arrays of which is
// basically a table with terminals 'down' and times 'across'
// Also save the max no of times for ANY terminus in window.maxTimes
//
// Format for each row (array)
// [0] Terminal name   This is split with | character to give group|name
// [1...n] trainTime objects
//----------------------------------------------------
  var tt,timesForTerm;
  var maxTimesForTerm = 0;
  var termName;
  var termNames = window.terminals;
  var rv = new Array();
  for(var i=0;i<termNames.length;i++){
    //GM_log(termNames[i]);
    var times = new Array(termNames[i]);  // full name ie Group letter | name
    termName = termNames[i].slice(2);
    timesForTerm = 0;
    for(var j=0;j<window.trainTimes.length;j++){
      tt = window.trainTimes[j];
      if(termName == tt.terminus){
        times.push(tt);
        timesForTerm = timesForTerm + 1;
      }
    }       
    // let us discover if any of these trains are running out of sequence
    // we can do this by looking at the expected times and checking
    // they are are increasing as we go through the array
    for(var k=1;k<times.length-1;k++){
      kExpected = times[k].Expected();
      if(kExpected >= 0){
        for(var m=k+1;m<times.length;m++){
          mExpected = times[m].Expected();
          if(mExpected>0){
            if(mExpected<kExpected){
              times[k].unsequenced = true;
              break;
            }
          }
        }          
      }
      // force for testing if(times[k].hhmm=='17:29'){times[k].unsequenced = true;GM_log('xxx');}
    }      
    rv.push(times);
    if(timesForTerm > maxTimesForTerm){maxTimesForTerm = timesForTerm;}
  }
  // save the max no of columns we're going to need in a global
  window.maxTimes = maxTimesForTerm;
  return rv;
};

//----------------------------------------------------
window.FlagGroupUnsequenced = function(Data){
// We look at all the departures *in a group* to
// see if there are any unsequenced when taken together
//----------------------------------------------------
  var tt,ttg,gix;
  var groups = new Array();
  var groupStr = '';
  
  for(var i=0;i<window.trainTimes.length;i++){  // run through all departures 
    tt = window.trainTimes[i];                  // in timetable order
    ttg = tt.Group();                 // find what group it belongs to
    gix = groupStr.indexOf(ttg);     // get an index so we can build group arrays
    if(gix==-1){                     // if not found then add a group array
      groupStr+=ttg;
      gix = groupStr.indexOf(ttg);
      groups[gix]=new Array();
    }
    groups[gix].push(tt);            // stick onto end of group
  }

  // modified version of routine used in BuildTermDataStructure
  for(gix=0;gix<groups.length;gix++){
    times = groups[gix];  
    for(var k=0;k<times.length-1;k++){
      kExpected = times[k].Expected();
      if(kExpected >= 0){
        for(var m=k+1;m<times.length;m++){
          mExpected = times[m].Expected();
          if(mExpected>0){
            if(mExpected<kExpected){
              times[k].unsequencedG = true;
              break;
            }
          }
        }          
      }
    }
  }
};  






//----------------------------------------------------
window.CreateNewTable = function(TimesData){
// We have the data in the correct order/structure
// and will now convert that into a table for display.
// Departing parameter is a boolean true if departures
//----------------------------------------------------


  // limit number of columns
  if(window.maxTimes >11){window.maxTimes=11;}
  
  // table heading
  var tfLabel,ttLabel,rowClass;
  var tableTop="<table class=bldb><tr class=bldbHead>";
  if(window.isDeparting){
    tfLabel = 'Terminus';
    ttLabel = 'Departures';
  }else{
    tfLabel = 'Origin';
    ttLabel = 'Arrivals';
  }  
  tableTop = tableTop + "<td class=headToFrom>"+tfLabel+"</td>";
  tableTop = tableTop + "<td class=headTimes colspan="+(window.maxTimes)+">"+ttLabel;
  tableTop = tableTop + "<div class=headInfo><span class=miClock>hh:mm</span><span class=miToGo>99</span><span class=miOperator>XX</span></div></td>";
  tableTop = tableTop + "</tr>";
 
  
  // each row
  var trow,rowStr,rowHtml='';
  var groupCode,groupLetter;
  for(var j=0;j<TimesData.length;j++){
    trow = TimesData[j];
    if(trow.length>1){  // only display terminal line if we have any actual data
      if((j % 2)===0){rowClass='row1';}else{rowClass='row2';}
      groupLetter = trow[0].slice(0,1);
      groupCode = '<input class=code type=text size=1 value="'+groupLetter+'">'; 
      rowStr = "<tr class="+rowClass+" ><td class=ToFrom>"+groupCode+trow[0].slice(2)+"</td>";
      for(var i=1;((i<trow.length)&&(i<=window.maxTimes));i++){
        rowStr = rowStr + "<td class=xtimes >" + DisplayTime(trow[i]) + "</td>";
      }
      rowStr = rowStr + "</tr>\n";
      rowHtml = rowHtml + rowStr;
    }  
  }

  // stick it all together
  return tableTop + rowHtml + "</table>";
};  
  
//----------------------------------------------
window.BuildNewTable = function(){
// We have already scraped the screen and got the terminus names
// Build data structure and create the required html 
//----------------------------------------------
  // Build the datastructure that we're going to need
  var termData = window.BuildTermDataStructure();
  window.FlagGroupUnsequenced(termData);
  return window.CreateNewTable(termData);
};

//----------------------------------------------------
function DisplayTime(TrainTime){
// All the HTML to display a trainTime object
// There are two parts:  time (hh:mm) and info
// The whole thing is wrapped in an <A> pointing to the train schedule
// We also create a hidden <div> with the text that will go into the
// pop-up calculator
//----------------------------------------------------
  // visible part
  var timeText  = TrainTime.hhmm;
  var infoText  = TrainTime.info;
  var timeClass = TrainTime.lateness;
  if(TrainTime.lost){timeClass = timeClass + ' xlost';} 
  if(TrainTime.actual===false){timeClass = timeClass + ' xprovisional';} 
  if(TrainTime.unsequenced===true){
    timeClass = timeClass + ' xunsequenced';
  }else{ 
    if(TrainTime.unsequencedG===true){timeClass = timeClass + ' xunsequencedG';} 
  }  
  var completeInfo = '<span class="'+timeClass+'"> '+infoText+'</span>';
  var aHref = '   <a href="'+TrainTime.trainDetailURL+'" class="x'+timeClass+'">';
  var display = aHref + timeText + completeInfo + "</a>";
  // hidden part
  var hidden = '';
  var exmin = TrainTime.Expected();
  var now = new Date();
  var nowHH = now.getHours();
  var nowMM = now.getMinutes();
  var nowMinutes = (60 * nowHH) + nowMM;
  var timeLeft = exmin - nowMinutes;
  if(exmin>=0){
    //if(timeClass != 'onTime'){
    // compute the expected time
    var exminMM = exmin % 60;
    var exminHH = ((exmin - exminMM) / 60) % 24;
    var ohh = (exminHH<10) ? '0' : '';
    var ohm = (exminMM<10) ? '0' : '';
    hidden = ohh+String(exminHH) + ':'+ohm+String(exminMM);
    if(timeLeft>2){hidden += '('+String(timeLeft)+' to go)';}
    if(timeLeft< -1){hidden += '('+String(0-timeLeft)+' overdue)['+exmin+']';}
  }
  if(infoText == 'CANX'){
    hidden = '';
    timeLeft = 0;
  }
  hidden += ' '+ TrainTime.opAcronym;
  if(hidden != ''){hidden = '<span class=hiddenpu>'+hidden+'</div>\n';}
 
  return display + hidden;
}  

  
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Initialise etc.
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

// =============================================================================
// Special feature for suggesting looking at BLDB page straight after installation
// We do this by hacking the vulpeculox.net/rail/index.htm page with the script
// itself.  This just sets a flag in the DIV.installed by setting the text to a space (instead of blank)
// =============================================================================
var isVulpeculox = (window.location.href.indexOf('vulpeculox')>=0);
if(isVulpeculox){
  document.getElementById('installed').innerHTML = ' ';
  return 0;  //   should hopefully crash JS as we don't want to bother with the rest 
} 



// What are the key dom elements?  We will be inserting our computed results
// in front of 'existing' and tbody contains the rows of raw data 
var existing = GetFirstMatchingElement("//div [@class='container full']",document);

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// CODE FOR STATION.  ARRIVING OR DEPARTING?  ALTERNATIVE DISPLAYS
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// are we arriving or departing
var aord = GetFirstMatchingElement("//div [@class='containertitlebig']/h2",document);
var aordtc = aord.textContent;
window.isDeparting = (aordtc.indexOf('PART')>0);
// code is on the end of text as (FOO)"
var i = aordtc.indexOf('(');
window.stationCode = aordtc.slice(i+1,i+4);
//GM_log('CODE = ' + window.stationCode);
var j = aordtc.indexOf(':');
window.stationName = aordtc.slice(j+2,i-1);
// timestamp,alt refresh link alternative a/d link
var nt = GetFirstMatchingElement("//div[@class='narrowtext']",existing);
var fulltstamp = nt.childNodes[1].childNodes[1].textContent;
i = fulltstamp.indexOf(':');
var shorttstamp = fulltstamp.slice(i-2,i+3);
//GM_log(shorttstamp+'|'+fulltstamp);
var artext = nt.childNodes[1].childNodes[3].textContent;
window.isRefreshing = (artext.indexOf('stop')===0);
var altRefreshURL = nt.childNodes[1].childNodes[3].href;

var ad = GetFirstMatchingElement("//div[@class='narrowtext']/ul/li/a",existing);
var altArrDepURL = ad.href;
//GM_log('REF : '+altRefreshURL);
//GM_log('DEP : '+altArrDepURL);






// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// GET ANY SERVICE BULLETINS
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
window.serviceBulletins = Array();
ApplyToMatchingElements("//div[@class='maintmessages']/div",existing,ScrapeServiceBulletin);
if(window.serviceBulletins.length > 0){
  var html = "<table class=sb><tr><td class=sbBig>!</td>";
  for(var i = 0;i<window.serviceBulletins.length;i++){
    html += "<td class=sbText>"+window.serviceBulletins[i]+"</td>";
  }  
  html += "<td class=sbMore>"+Button('More',window.servBullURL)+"</td>";
  
  html += "</tr></table>";
  var sb = document.createElement('div');
  sb.innerHTML=html;
  sb.id='servBull';
  existing.parentNode.insertBefore(sb,existing);
}

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// CREATE MAIN TITLE etc
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
var ADaltThisPage='';
var REFaltThisPage='';
var asAt='17:05';

var html = '<table class=title><tr><td class=station>'+window.stationName+'</td><td class=aord>';
if(window.isDeparting){
  html += 'Departures ' + Button('Arrivals',altArrDepURL);
}else{
  html += 'Arrivals ' + Button('Departures',altArrDepURL);
}  
html += '</td><td class=refreshing><span class=titleTime>'+shorttstamp+'</span> ';
if(window.isRefreshing){
  html += 'Page automatically refreshes '+ Button('Non-refreshing version',altRefreshURL);
}else{
  html += 'Static page '+ Button('Refreshing version',altRefreshURL);
}
html += '</td></tr></table>';
var mt = document.createElement('div');
mt.innerHTML=html;
mt.id = 'titleBLDB';
existing.parentNode.insertBefore(mt,existing);

// Globally accessible array for all of our trainTime objects
window.trainTimes = new Array();
  
  
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// GET ALL THE ROWS OF RAW INFORMATION
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// This looks at each row of raw data in turn, passes it to
// ConvertRowToData() which hacks it into a more useful form
// and adds them (in order) to window.trainTimes array

// we may have a second table following on from the first
// with bustitutions.  Flag this modal change
window.bustitution = false;

ApplyToMatchingElements("//table [@class='arrivaltable']/tbody/tr",existing,ConvertRowToData);
//GM_log('tt list length:' + window.trainTimes.length);




// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// CONVERT TIMETABLE ORDER TO TERMINUS ORDER
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Read local persistent store and merge with any new names scraped from page
GetTerminusNames();
var html = window.BuildNewTable();

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// BUILD THE NEW TABLE AND INSERT IT AT THE TOP
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
window.newElement = document.createElement('div');
window.newElement.innerHTML=html;
window.newElement.id = 'tableBLDB';
existing.parentNode.insertBefore(window.newElement,existing);
window.moreInfo = GetFirstMatchingElement("//div [@class='headInfo']",document);

window.InstallEvents();

var clockImg      = "data:image/png,%89PNG%0D%0A%1A%0A%00%00%00%0DIHDR%00%00%00%10%00%00%00%10%08%06%00%00%00%1F%F3%FFa%00%00%00%07tIME%07%D9%0C%18%11%050%EF%F2x%98%00%00%00%09pHYs%00%00%0B%12%00%00%0B%12%01%D2%DD~%FC%00%00%01%BCIDATx%DA%A5%93%3FK%C3P%14%C5%AFR%E9%17%A8%10j%A3%D8%AA%05%B7%2CB%15%5DTp%A8%BB_%A0I%87%82%B88%3A%E4%03%B8%D8%A1M%5D%05%C1E%B0%83%82%D2%C9%3F%8B%14A%A4iK%B4%06%24C7u(MR%DFILHk%5D4p%DA%E4%DE%DF9%BC%FB%5E2%D2%EB%F5%E8%3FW%08%3F%8A%A2%04k%F3L%5BLi%26%E1%BBVe*3%1D3%3Dy%A0(%8An%80m%DB%94%CDf%A9P(d%C2%E1%B0%9CL%26%B9x%3CN%1C%C79%A0a%18%82%A6i%82%AA%AA%99N%A7%B3%C7%D8%12c%9D%DE%08F%C8%E7%F3%B8%CFD%22%11%25%B5%B8D%3C%CF%0F%5D%AE%AE%EBt%7BsM%EDv%5Bd%8F%A5%5C.G%A3hX%965%1F%1A%1B%93%17R%8B%14%8D%F1d%B1m%81jj%DD%91%F7%8C%1E%18%B0%F0%C0%EB%04t%BB%DD%AD%C4%CC%1C%17%9D%98%24%CB%26_l%C9%8E%8250%60%E1%F1%03L%D3LO'f%FB%40%08%07%04%0D%D6%C1%C2%E3%9F%02K%13%C6%B9%18%99v%FF%CC%8DF%DD%F9_%1D%A8%83%85'%18%40%E6%90%D7Ak%BA%01%BB%DB%92%1B%B4%91f%DA%24%CF%13%0C%A8%BE%B6ZB%94%9F%F2%CD%95%8B3%92%F7%8B%3FB%B1%CA7%BD%E5x%82%7BP%AE%3D%3E%F83%5E%9D%9F%D1%CA%FA%E6%8F%D9%3D%81%85'x%0A%C7%0F%F7w%86%D6pw%7Cy%EDw3%18%B0%F0%F8%01%ECL%9F%3E%3F%DE%F7*%E7%A7l%EE%9A%7F%EE%83B%0F%0CXx%82%23%D0%8E%7CPz%D3_%C4%D3%A3C%E3%B2%7CB%AF%CFM%B2%99%09%C2%3Dj%E8%81%01%0B%8F%FF*K%92%F4%A7%8F%A9X%2C%BA%01%FF%B9%BE%00%5D%CC%3F%B6D%3F%91%D7%00%00%00%00IEND%AEB%60%82";
var hourglassImg  = "data:image/png,%89PNG%0D%0A%1A%0A%00%00%00%0DIHDR%00%00%00%0B%00%00%00%10%08%02%00%00%00O%DF%12)%00%00%00%15tEXtCreation%20Time%00%07%D9%0C%18%109%17%ED%C8%3DK%00%00%00%07tIME%07%D9%0C%18%11%04%23rW%08%07%00%00%00%09pHYs%00%00%0B%12%00%00%0B%12%01%D2%DD~%FC%00%00%01_IDATx%DAcdca%F8%F3%97%01%17%60bb%60aaa((%C8%F5%F1%0B%C1%94%3Ew%F6TuU)%0B%0B3%83%AA%AA%B2%AD%AD%1D%A6%8A%BB%B7%AF%01m%60%02%E2%EB%97O%FE%FF%FF%17%9B%19'9%D9%19%18%25%85%19%3E~ex%BES%8AW%26%88A8%98%81S%9D%E1%EBE%86%AF%E7%BF%BF%3D)j%B1QF%94%81%E1%FF%FF%FF%5C%1C%0C%ADi%0C%7F%0F2%FC%3B%84%40%ED%E9%0C%40q%A0%2C%0B%D0%B4%AF%DF%FFss2%0A%F32%A4%F9B%CD_%B2%9B%A1y!H%1C%C4%F9%0F%03%40%1D%EB%DBx%FE%EDc%D8%DE%C3%03%D1%0D%01L%C8N%131%9D%C5%C0m%22%604%13%C5%C1p%03%F6l%9E%0Bd%FC%FB%F9%0AH%1E%3D%B4%03n%0C%D4%A5%B3%FAK%FF%A3%82%A5%F3z%20%8A%40%0E%B6%D2%13%FE%FA%F9%1D%C8%80%F7'%FF%1D%B5%FE%F7d%D9%FF%7F%FF%FE%FC%F9%ED%60%A9%01%94e%E2depr%0F%E1%E4%E4%FC%7F%BD%86%E1%B05%C3%EB%A3%0Cg%A2%FE_%CAd%FE%F7%CD%D4%DCV%88%9B%81%E5%F7%1F%06%19%A1%3F%0C%07-%19%3E%5E%40%B8%EE%DE%CC%FF%AF%0Fk%CBX~%FB%C9%C0%02%8C%D8%93%BB%E6%0A%BD%01Z%09%92bD%A8%BAv%EA%DA%B5%1F%BF%18%18y8%40%B1%FF%1FG%EC331%00%00%23%09%C0i%E8.f%3F%00%00%00%00IEND%AEB%60%82";

var css = 'table.bldb    {border:1px solid olive;margin-bottom:2px;}'+
          'table.sb      {border:3px solid red;color:red}'+
          'table.bldbh   {background-color:#bfd;margin:0px;}'+
          'table.title   {background-color:#bcf;color:white;font-weight:bold;margin-bottom:2px;}'+
          'td.headToFrom {background-color:#bcf;}'+
          'td.headTimes  {background-color:#bcf;}'+
          'td.ToFrom     {color:#003366;width:25%;}'+
          'td.xtimes     {font-weight:normal;}'+
          'td.sbBig      {font-weight:bold;font-size:250%;color:red;}'+
          'td.sbText     {padding-bottom:5px;}'+
          'td.station    {font-size:180%;color:#026;}'+
          'td.aord       {font-size:150%;color:black;}'+
          'td.refreshing {text-align:right;color:black;}'+
          'table.bldb a  {display:block;text-decoration:none;}'+
          'table.bldbh a {display:block;text-decoration:none;}'+
          'table.bldb a:hover       {background-color:#bcf;}'+
          'input.code    {border:1px solid white;background-color:#fc8;width:1em;margin:0px 3px 0px 3px;}'+
          'a             {text-decoration:none;}'+
          'a.xlost       {background-color:#fbb;}'+
          'a.xvery       {color:red;}'+
          'a.xunsequenced     {border:1px dotted red !important}'+
          'a.xunsequencedG    {border:1px dotted blue !important}'+
          'a.xonTime     {font-weight:lighter;color:green}'+
          'a.xprovisional{font-style:italic; color:black !important;}'+
          'a.xbus        {color:#f0f !important;}'+
          'a.navBut1          {font-size:10px;font-weight:normal;text-decoration:none;margin-left:1em;margin-right:1em;background-color:#eee;-moz-border-radius:4px;border: 2px outset #577FB0;padding:0px .5em 0px .5em;}\n'+
          'a.navBut1:link     {}\n'+
          'a.navBut1:visited  {}\n'+
          'a.navBut1:hover    {color:#577FB0}\n'+
          'a.navBut1:active   {border: 2px inset #577FB0;}\n'+
          'span.titleTime{font-size:150%;color:white;font-weight:bold;background-color:black;-moz-border-radius:3px;margin-right:3em;}'+
          'span.bit      {margin-left:3px;color:#b51;}'+
          'span.bus      {margin-left:3px;color:#f0f;}'+
          'span.very     {margin-left:3px;color:red;}'+
          'span.early    {margin-left:3px;background-color:green;color:white;}'+
          'span.miClock  {min-width:45px;margin-left:8px;padding-left:16px;background-repeat:no-repeat;background-position:left;background-color:#ddd;background-image:url("'+clockImg+'")}'+
          'span.miToGo   {min-width:75px;margin-left:8px;padding-left:16px;background-repeat:no-repeat;background-position:left;background-color:white;background-image:url("'+hourglassImg+'");}'+
          'span.miOperator  {min-width:55px;margin-left:8px;background-color:#ddd;display:inline;}'+
          'div.about     {text-align:right;color:green;font-size:80%;font-weight:light;}'+
          'div.headInfo  {float:right;text-align:right;color:black;display:inline;visibility:hidden;}'+
          'span.hiddenpu {display:none;}';
AddStyle(css);

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// MANAGE LIST OF PREVIOUSLY USED LDBS
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
LoadPreviousStations();
AddStationToPrevious(window.stationName,window.location.href);
SavePreviousStations();
var buttons = BuildPreviousStationButtons(window.stationName);
var bu = document.createElement('div');
bu.innerHTML=buttons;
existing.parentNode.insertBefore(bu,existing);


// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// BUILD A HELP PANEL
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
help = "<table class=bldbh><tr><td><b>Legend</b></td>";
help += "<td><a name='x' class=xonTime >On time</a></td>";
help += "<td><a name='x' class=xprovisional >Timetabled. No data yet</a></td>";
help += "<td><a name='x' class=xbus >Bus replacement</a></td>";
help += "<td><a name='x' class=xlost >Expected data missing (delay is guesswork)</a></td>";
help += "</tr><tr>";
help += "<td><b>Group stations</b></td>";
help += "<td colspan=4>Hover mouse over <input class=code value='?'> then press A-Z. Stations are sorted alphabetically</td>";
help += "</tr><tr>";
help += "<td><b>Out of sequence trains</b></td>";
help += "<td colspan=2><a name='x' class=xunsequenced>May arrive/depart later than 'following' from/to same place</a></td>";
help += "<td colspan=2><a name='x' class=xunsequencedG>May arrive/depart later than 'following' in same group</a></td>";
help += "</tr></table>";
help += "<div class=about>Better LDB Version "+window.versionNumber+" ("+window.versionDate+")  Written by Peter 'Prof' Fox <a href='http://vulpeculox.net'>vulpeculox.net</a></div>";

window.helpElement = document.createElement('div');
window.helpElement.innerHTML=help;
existing.parentNode.insertBefore(window.helpElement,existing);

// finished!
