Visualise Orienteering Splits

allow orienteering splits visualisation

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(У мене вже є менеджер скриптів, дайте мені встановити його!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Visualise Orienteering Splits
// @namespace    http://watsons.id.au
// @version      2.7.000
// @description  allow orienteering splits visualisation
// @include      http://obasen.orientering.se/winsplits/online/*/default.asp*
// @include      http://wmoc2016.ee/2016/wmoc2016/longf/split*
// @include      http://act.orienteering.asn.au/eventor/results/splits/*
// @author       Arthur Watson
// @copyright    2017+ GPL
// @grant        none
// @run-at-document-start
// ==/UserScript==
/*
 @require http://http://members.iinet.net.au/[email protected]/visualiser/visualise_splits.user.js
 @require file:///home/watsar/public_html/tampermonkey/visualise_splits.user.js
 @include      http://act.orienteering.asn.au/gfolder/results/*
*/

//"use strict";
/*jslint
   browser: true, continue: true, indent: 2, regexp: true, white: true, sloppy: true
*/

var
  visLiterals = {

  },
  visConstants = {
    secondsPerMinute: 60,
    secondsPerHour: 3600,
    minutesPerHour: 60
  },
  htmlHead = function() {/*
    <meta charset="utf-8">
    <meta name="description" content="Visualise Orienteering Split Times">
    <meta name="author" content="Arthur Watson">
    <meta name="version" content="2.1">
    <meta name="last modified" content="04 Sep 2017">
    <meta name="robots" content="noindex, nofollow">
    <title>Visualise Orienteering Split Times</title>
*/}.toString().slice( 15, -3),
  htmlBody = function() {/*
    <div class="container">
      <div class="headers">
        <h1 class="aardvark">Orienteering Splits Time Visualiser</h1>
        <h2 class="aardvark" id="description"></h2>
      </div>
      <!-- <div class="box">
        <button class="splits" type="submit" onclick="drawSplits();">1. Begin Here!</button>
      </div> -->
      <div class="box">
        <div id="courseclasslistcell"></div>
        <div id="showrunnerbuttoncell"></div>
      </div>
      <div class="box">
        <div id="runnerlistcell"></div>
        <div id="visualisebuttoncell"></div>
      </div>
      <div class="box">
        <button id="helpButton" class="splits" type="submit" onclick="window.open( 'http://members.iinet.net.au/[email protected]/visualiser/documentation.html',target='_blank','height=500,width=400');">Show Help.</button>
        <!-- <button id="helpButton" class="splits" type="submit" onclick="interact.displayHelp();">Show Help.</button> -->
        <!-- <div id="showhelp" class="hide box"></div> -->
      </div>

    <div class="courses" id="tableSplits"></div>
    <div class="graph" id="visualSplits"></div>
    <div class="declaration" id="source"><p>Produced from published data by SOURCE. [Aardvark Systems 2017]</p></div>
    <input type="hidden" id="eventdata" onchange="drawSplits();" value="">
*/}.toString().slice( 15, -3);

function fixTime( t) {
  var
    items = [],
    seconds = 0;

  // remove the leg place [ n] for WMOC
  t = t.replace( /\[.*?\]/, '');

  items = t.split( /[.:]/);

  switch ( items.length) {
  case 2:
    // m:s, so will be OK but change . to :
    t = items[ 0] + ':' + items[ 1];
    seconds = parseInt( items[ 0], 10) * 60 + parseInt( items[ 1], 10);
    break;
  case 3:
    // h:m:s
    t = parseInt( items[ 0], 10) * 60 + parseInt( items[ 1], 10);
    t = t.toString() + ':' + items[ 2];
    seconds = parseInt( items[ 0], 10) * 3600 + parseInt( items[ 1], 10) * 60 + parseInt( items[ 2], 10);
    break;
    default:
      if ( !isNaN( parseInt( t, 10))) {
        t = '0:' + t;
        seconds = parseInt( t, 10);
      }
  }
  return [ t, seconds];
}

function timeToSeconds( t) {
  // transform the [h:]mm.ss on the WinSplits page --> seconds

  var
    items = [],
    seconds = 0;

  items = t.split( /[.:]/);

  switch ( items.length) {
  case 2:
    // m.s
    seconds = parseInt( items[ 0], 10) * visConstants.secondsPerMinute +
              parseInt( items[ 1], 10);
    break;
  case 3:
    // h:m.s
    seconds = parseInt( items[ 0], 10) * visConstants.secondsPerHour +
              parseInt( items[ 1], 10) * visConstants.secondsPerMinute +
              parseInt( items[ 2], 10);
    break;
    default:
      if ( !isNaN( parseInt( t, 10))) {
        seconds = parseInt( t, 10);
      }
  }
  return seconds;
}

function timeFormat( t) {
  // transform the [h]:mm.ss on the WinSplits page --> mmm:ss

  var
    items = [];

  items = t.split( /[.:]/);

  switch ( items.length) {
  case 2:
    // m:s, so will be OK but change . to :
    t = items[ 0] + ':' + items[ 1];
    break;
  case 3:
    // h:m:s --> m:s
    t = parseInt( items[ 0], 10) * visConstants.minutesPerHour +
        parseInt( items[ 1], 10);
    t = t.toString() + ':' + items[ 2];
    break;
    default:
      if ( !isNaN( parseInt( t, 10))) {
        t = '0:' + t;
      }
  }
  return t;
}

function pad( str, max) {
  return str.length < max ? pad( "0" + str, max) : str;
}

function secondsToTime( s) {
  // convert seconds to mmm:ss
  var min, sec;

  min = ( Math.floor( s / 60)).toString();
  sec = pad( (s - ( min* 60)).toString(), 2);

  return min + ':' + sec;
}

function stripHeader( header) {
  // change the header from S-1 (135) to 1, & 11-F to 11
  var regex = /^[\w\W]*\-\s*(\d+)[\w\W]*$/;
  if ( header.indexOf( 'F') > -1) {
    header = 'F';
  }
  else {
    header = header.replace( regex, '$1');
  }
  return header;
}

function stripWhitespace( text) {
  // remove leading and trailing whitespace
  var regex = /\s*(.*?)\s*$/;  // non greedy match of non-whitespace
  text = text.replace( regex, '$1');
  return text;
}

function processMessage ( message, ageClass, eventTitle) {
  var
    i,
    j,
    runnerIndex,
    results = {};

  // fix ageclass
  ageClass = ageClass.replace( /\s+\([\w\s]*\)/, '');

  results.description = eventTitle;
  results.courses = [];
  results.courses[ 0] = {};
  results.courses[ 0].name = 'Course: - Not Stated';
  results.courses[ 0].ageclasses = [];
  results.courses[ 0].ageclasses[ 0] = ageClass;

  results.courses[ 0].controlcodes = [];
  for ( j = 1; j < message[ 1].length - 1; j += 1) { // just the control sequence really
    results.courses[ 0].controlcodes[ j - 1] = stripHeader( message[ 1][ j]);
  }

  results.courses[ 0].runners = [];
  runnerIndex = 0;
  for ( i = 2; i < message.length; i += 2) {  // process every second line
    if ( fixTime( message[ i][ 2])[ 1] < 1 ||
         isNaN( fixTime( message[ i][ 2])[ 1])) { // ignore dnf's etc
      continue;
    }
    results.courses[ 0].runners[ runnerIndex] = {};
    results.courses[ 0].runners[ runnerIndex].name = message[ i][ 1];
    results.courses[ 0].runners[ runnerIndex].ageclass = ageClass;
    results.courses[ 0].runners[ runnerIndex].time = timeFormat( message[ i][ 2]);
    results.courses[ 0].runners[ runnerIndex].splits = {};
    results.courses[ 0].runners[ runnerIndex].splits.raw = [];
    for ( j = 4; j < message[ i].length - 1; j += 2) {  // and only every 2nd is a split
      results.courses[ 0].runners[ runnerIndex].splits.raw.push( fixTime( message[ i][ j])[ 0]);
    }
    runnerIndex += 1;
  }

  return results;
}

function processWMOCMessage ( message, ageClass, eventTitle) {
  var
    i,
    j,
    runnerIndex,
    results = {};

  results.description = eventTitle;
  results.courses = [];
  results.courses[ 0] = {};
  results.courses[ 0].name = 'Course: - Not Stated';
  results.courses[ 0].ageclasses = [];
  results.courses[ 0].ageclasses[ 0] = ageClass;

  results.courses[ 0].controlcodes = [];
  var code = 1;
  for ( j = 7; j < message[ 1].length - 1; j += 1) { // wmoc doesn't give codes so just the numbers
    //results.courses[ 0].controlcodes[ j - 7] = message[ 1][ j];
    results.courses[ 0].controlcodes[ j - 7] = code++;
  }
  results.courses[ 0].controlcodes[ message[ 1].length - 7] = 'F';

  results.courses[ 0].runners = [];
  runnerIndex = 0;
  for ( i = 2; i < message.length; i += 2) {  // process every two line pair
    if ( fixTime( message[ i][ 5])[ 1] < 1 ||
         isNaN( fixTime( message[ i][ 5])[ 1])) { // ignore dnf's etc
      continue;
    }
    results.courses[ 0].runners[ runnerIndex] = {};
    results.courses[ 0].runners[ runnerIndex].name = stripWhitespace( message[ i][ 2]);
    results.courses[ 0].runners[ runnerIndex].ageclass = ageClass;
    results.courses[ 0].runners[ runnerIndex].time = timeFormat( message[ i][ 5]);
    results.courses[ 0].runners[ runnerIndex].splits = {};
    results.courses[ 0].runners[ runnerIndex].splits.raw = [];
    for ( j = 7; j < message[ i].length; j += 1) {  // the second line has the splits
      results.courses[ 0].runners[ runnerIndex].splits.raw.push( fixTime( message[ i + 1][ j])[ 0]);
    }
    //console.log( results.courses[0].runners[runnerIndex].name + ", " +
    //             results.courses[0].runners[runnerIndex].time + " --> " +
    //             results.courses[ 0].runners[ runnerIndex].splits.raw);
    runnerIndex += 1;
  }

  return results;
}

function setupNewWindow( oEvent) {
  var
    scriptSource = 'http://members.iinet.net.au/[email protected]/visualiser/',
    //scriptSource = 'http://localhost/~watsar/tampermonkey/',
    splitsElements,
    splitsVisualiser,
    scriptElement,
    i,
    j;

    splitsElements = [
       { 'element':'script', 'attributes': [[ 'src', scriptSource + 'draw_splits.js'],
                                            [ 'type', 'text/javascript']]
       },
       { 'element':'script', 'attributes': [[ 'src', 'http://d3js.org/d3.v3.min.js'],
                                            [ 'charset', 'utf-8']]
       },
       { 'element':'link',   'attributes': [[ 'href', scriptSource + 'draw_splits.css'],
                                            [ 'type', 'text/css'],
                                            [ 'rel', 'stylesheet']]
       }];


    splitsVisualiser = window.open( '', 'Splits Visualiser', 'left=100,top=100,width=1200,height=800,menubar=no,titlebar=no,location=no,scrollbars=yes', 'POS');
    splitsVisualiser.document.head.innerHTML = htmlHead;

    for ( i = 0; i < splitsElements.length; i += 1) {
      scriptElement = splitsVisualiser.document.createElement( splitsElements[ i].element);
      for ( j = 0; j < splitsElements[ i].attributes.length; j += 1) {
        scriptElement.setAttribute( splitsElements[ i].attributes[ j][ 0], splitsElements[ i].attributes[ j][ 1] );
        splitsVisualiser.document.head.appendChild( scriptElement);
      }
    }

    splitsVisualiser.document.body.innerHTML = htmlBody;
    //splitsVisualiser.document.body.setAttribute( 'onload', "drawSplits();");
    splitsVisualiser.document.title = 'Orienteering Event - Splits Visualiser --> ' + oEvent.description;
    splitsVisualiser.document.getElementById( 'eventdata').setAttribute( 'value', JSON.stringify( oEvent));

  return 0;
}

function validateMessage( message) {
  var headerLine;

  headerLine = message[ 0].join( ',');
  headerLine = headerLine.replace( /\s/g, '');

  //console.log( 'header: ' + headerLine);

  if (    headerLine.match( /^Pos,Name,Finishtime,Diff,legtot/) &&
          headerLine.match( /legtot,Name$/)) { return true;}

  return false;
}

// The following are for extracting from OACT web pages

// check that this is a splits result from Stephan Kramer SportSoftware
function getResultHeaders( headerRow) {
  var i = 0,
      count = 0,
      headers = {},
      t = '',
      cells = headerRow.querySelectorAll( 'th');

  for ( i = 0; i < cells.length; i += 1) {
    t = cells[ i].textContent;
    if ( t.length > 0) {
      headers[ t] = i;
      count += 1;
    }
    else {
      break;
    }
  }
  headers.length = count;
  return headers;
}

function getEventData() {
// preliminary scan through the results page to get:
//    1. the name and date of the event
//    2. the names of the course
//    3. how many controls in each
//    4. how many runners in each
  var event = {},
      eventDescription,
      courseNameCells,
      courseLengthCells,
      courseNumberOfControlCells,
      courseName,
      courseLength,
      courseNumberOfControls,
      courseNumberOfRunners,
      courseNamePattern = /\s+\(\d+\)$/,  // to get the number of runners in the parenthised bit at the end
      runners,
      i;

  eventDescription = document.querySelector( 'div#reporttop table tr td nobr').textContent;
  event.description = eventDescription ;

  courseNameCells            = document.querySelectorAll( 'td#c00');
  courseLengthCells          = document.querySelectorAll( 'td#c01');
  courseNumberOfControlCells = document.querySelectorAll( 'td#c02');

  event.courses = [];

  for ( i = 0; i < courseNameCells.length; i += 1) {
    event.courses[ i] = {};
    // course name field is <course_name> <(no_of_runners)>
    courseName = courseNameCells[ i].textContent;
    runners = courseNamePattern.exec( courseName);
    courseName = courseName.replace( runners[ 0], '');
    courseName = courseName.replace( /\s+/, '_');
    runners = /\d+/.exec( runners[ 0]);
    courseNumberOfRunners = runners[ 0];

    event.courses[ i].name = courseName;
    event.courses[ i].numberofrunners = courseNumberOfRunners;
    event.courses[ i].ageclasses = [];

    // take the Km off the end of the length
    courseLength = courseLengthCells[ i].textContent;
    courseLength = courseLength.replace( /\s+km/i, '');

    event.courses[ i].length = courseLength;

    // and take off the 'C' and the end of this field
    courseNumberOfControls= courseNumberOfControlCells[ i].textContent;
    courseNumberOfControls = courseNumberOfControls.replace( /\s+c/i, '');

    event.courses[ i].controls = courseNumberOfControls;
  }

  return event;
}

function getRunnerData( runnersRowIndex, rowCells, resultHeaders, runnersData) {
  //  grab name, ageclass and time from the first runner row
  //  the club from the second
  // the indices are in the result headers in the event object
  if ( runnersRowIndex === 0) {
    // get name, ageclass and time
    runnersData.Pl       = rowCells[ resultHeaders.Pl].textContent;
    runnersData.name     = rowCells[ resultHeaders.Name].textContent;
    runnersData.ageclass = rowCells[ resultHeaders[ 'Cl.']].textContent;
    runnersData.time     = rowCells[ resultHeaders.Time].textContent;
  }
  else if ( runnersRowIndex === 1) {
    if ( typeof runnersData === "undefined"){
      runnersData = {};
      runnersData.time = 'undef';
    }
    else {
      // get the club, in the same position as name in row 0
      runnersData.club = rowCells[ resultHeaders.Name].textContent;  // club is in same position as name in the previous row
    }
  }
  return runnersData;
}

function getRunnerSplits( rowCells, resultHeadersLength, runnersSplits) {
  // get the splits from odd numbered rows
  var i;

  for ( i = resultHeadersLength; i < rowCells.length; i += 1) {
    if ( rowCells[ i].textContent.length > 0) {
      runnersSplits.push( rowCells[ i].textContent);
    }
    else {
      break;
    }
  }

  return runnersSplits;
}

function extractResults( event, rows, courseIndex, columnLength) {
  // the first row tuple has the control numbers
  // then we have tuples of double the length for the results which have
  // split and cumulative times on alternating rows
  // also there are short rows which we will ignore
  // also stop scanning when we get to non-finishers, ie the time is not in the correct format
  var cellLength,
      controlsPerRow,
      rowsForControls,
      rowsForRunner,
      rowCells,
      controlCodes,
      thisCode,
      controlCodePattern,
      runnersSplits,
      runnersData,
      splitsIndex,
      runnersIndex,
      runnersRowIndex,
      resultHeaders,
      rowContent,
      i,
      j;

  // check that the column length and number of cells tally
  cellLength = rows[ 0].querySelectorAll( 'td').length;
  if ( columnLength === cellLength) {
    event.checkCellLength[ courseIndex] = true;
  }
  else {
    event.checkCellLength[ courseIndex] = false;
    return event;
  }

  controlsPerRow = columnLength - event.courses[ courseIndex].resultHeaders.length;
  rowsForControls = Math.ceil(( parseInt( event.courses[ courseIndex].controls, 10) + 1) / controlsPerRow);  // need an extra cell for last control to finish
  rowsForRunner = rowsForControls * 2;
  //alert( 'course ' + event.courses[ courseIndex].name + ': columns ==> ' + columnLength + ', rowsForControls ==> ' + rowsForControls + ', rowsForRunners ==> ' + rowsForRunner);

  controlCodes = [];
  controlCodePattern = /\(\d+/;

  for ( i = 0; i < rowsForControls; i += 1) {
    rowCells = rows[ i].querySelectorAll( 'td');
    for ( j = event.courses[ courseIndex].resultHeaders.length; j < rowCells.length; j += 1) {
      thisCode = rowCells[ j].textContent;
      if ( controlCodePattern.test( thisCode)) {
        thisCode = controlCodePattern.exec( thisCode);
        thisCode = thisCode[ 0].replace( '(', '');
      }
      controlCodes.push( thisCode);
      if( /F/.test( thisCode)) { break;}  // no more controls after the finish
    }
    if( /F/.test( thisCode)) { break;} // and break from the outer loop
  }
  event.courses[ courseIndex].controlcodes = controlCodes;

  // now get the runners' results
  splitsIndex = 0;
  runnersIndex = 0;
  runnersRowIndex = 0;
  resultHeaders = event.courses[ courseIndex].resultHeaders;
  event.courses[ courseIndex].runners = [];

  for ( i = rowsForControls; i < rows.length; i += 1) {
    rowCells = rows[ i].querySelectorAll( 'td');
    if( rowCells.length < columnLength) { continue;}  // there are short rows used as spacers that can be ignored

    // also some rows  that indicate controls visited that weren't on the course
    // these have an attribute of style="font-style: italic;"
    rowContent = '';
    for ( j = resultHeaders.length; j < columnLength; j += 1) {
      if ( rowCells[ j].getAttribute( 'style') === 'font-style: italic;') {  // set the content to empty string
        rowCells[ j].textContent = '';
      }
      if ( /\-+/.test( rowCells[ j].textContent)) { // make unknowns ( '-----' ) zero time
        rowCells[ j].textContent = '0:00';
      }
      if ( !/\d+:\d+/.test( rowCells[ j].textContent)) { // set non-times to empty string
        rowCells[ j].textContent = '';
      }
      rowContent += rowCells[ j].textContent;
    }

    if ( rowContent.length < 1) {
      continue;
    }

    if( /\d+/.test( rowCells[ 0].textContent)) { // this should be the runners place
      // start a new runner
      runnersRowIndex = 0;
      runnersData = {};
      runnersSplits = [];
      runnersData = getRunnerData( runnersRowIndex, rowCells, resultHeaders, runnersData);
    }

    if ( runnersRowIndex === 1) {
      runnersData = getRunnerData( runnersRowIndex, rowCells, resultHeaders, runnersData);
      // do this to prevent funnies for undefined splits and messy data
      if ( !( /\d{1,4}:\d{1,}/.test( runnersData.time))) { // if not a valid time mm:ss then skip this runner
        runnersRowIndex = rowsForRunner + 1; // this will force a skip below
        runnersData = {};     // and the data and splits
        runnersSplits = [];
      }
    }


    if( runnersRowIndex % 2 !== 0) {
      // for odd rows just get the splits
      runnersSplits = getRunnerSplits( rowCells, resultHeaders.length, runnersSplits);
    }

    // increment the runner row index
    runnersRowIndex += 1;

    if ( runnersRowIndex === rowsForRunner) {
      if ( runnersSplits.length === ( parseInt( event.courses[ courseIndex].controls, 10) + 1)) {
        runnersData.splits = {};
        runnersData.splits.raw = runnersSplits;  // add the splits to the runner

        // add the ageclass to the course if not there already
        if ( event.courses[ courseIndex].ageclasses.indexOf( runnersData.ageclass) < 0) {
          // add the ageclass to the course
          event.courses[ courseIndex].ageclasses.push( runnersData.ageclass);
        }
        event.courses[ courseIndex].runners.push( runnersData); // add the runner to the course
      }
      runnersRowIndex = 0;  // reset the runner row index
      runnersData = {};     // and the data and splits
      runnersSplits = [];
    }
    else if ( runnersRowIndex > rowsForRunner) {
      continue;
    }
  }

  return event;
}

function getResults( event) {
  // now go through the page and grab the runner sin each course by ageclass
  //   so we get:
  //    ageClass[ i] = [ runner[ 0], runner[ 1], ... runner[ max]],
  //     runner[ i]   = { name: 'name', 'time', splits: []}
  //       splits       = [ split[ 0], split[ 1], ... split[ F]]

  // how the page is organised:
  //  there are groups of three tables for each course:
  //    1. table with one row of course info, we've already got this but it is useful as a place marker.
  //    2. a table with one row of headers for the results, this will tell us how many headers there
  //       are since sometimes some fields are optional. Note that these are 'th' not 'td'.
  //    3. a table with the results, the first rows in this are the control numbers so the number of controls
  //       in this course is useful since we can work out how many rows we need for the headers plus the controls.
  //       we can get the width of the table from the number of cells but there is also a 'col' group that is
  //       convenient.
  //      Some of the rows here are short and must just be spacers so we need to drop them so that we can
  //      run through the runner data without hiccuping.
  //      Some results of team members don't have split times just '---' so we can ignore them too.
  //      Once we reach runners with a time not in a [hh:]mm:ss format we can stop since we can't compare
  //      non finishers.
  // method:
  //  get all relevant tables by querySelector( table[width]:not([width=""]"), the first is the reporttop which
  //  we've already looked at. so start at the second. There shoudl be three per course.
  //  check that this is a course description by it having a 'td#c00'. if not abort and report an error.
  //  do a couple of nextSibling to get the results headers table.
  //  work out how many headers there are.
  //  do a nextSibling to get the results.
  //  count the columns, either by the count of querySelectorAll( 'col') and / or count the 'td's in a row.
  //  do both and check that we get the same answer, abort if we don't with an error message.
  var tables,
       rows,
       //cells,
       courseIndex,
       //resultHeaders,
       columnLength,
       i;

  tables = document.querySelectorAll( 'table[width]:not([width=""])');

  // check that there are three times number of courses plus one
  if (( event.courses.length * 3 + 1) === tables.length) {
    event.checkCourseNumber = true;
  }
  else {
    //console.log( 'There are ' + tables.length + ' tables, and ' + event.courses.length + ' courses, there should be ' + ( event.courses.length * 3 + 1) + ' tables!.');
    event.checkCourseNumber = false;
    return event;
  }

  // preset the courseIndex
  courseIndex = -1;

  // initialise some event properties
  event.checkCellLength = [];

  // run through the tables
  for ( i = 1; i < tables.length; i += 1) {
    // check which table we are processing
    rows = tables[ i].querySelectorAll( 'tr');
    if ( rows[ 0].querySelector( 'td#c00') !== null) {
      // this is the course info header, set the index and skip
      courseIndex += 1;
    }
    else if ( rows[ 0].querySelector( 'th') !== null &&
              rows[ 0].querySelector( 'th').textContent === 'Pl') {
      // this is the results header, so count the header cells and
      // evaluate their indices
      event.courses[ courseIndex].resultHeaders = getResultHeaders( rows[ 0]);
      //alert( 'result headers for course ' + event.courses[ courseIndex].name + ': ' + JSON.stringify( event.resultHeaders[ courseIndex]));
    }
    else if ( rows[ 0].querySelector( 'td#c11') !== null) {
      // now we have the results table
      columnLength = tables[ i].querySelectorAll( 'col').length;
      event = extractResults( event, rows, courseIndex, columnLength);
    }
  }
  return event;
}

function getEventData() {
// preliminary scan through the results page to get:
//    1. the name and date of the event
//    2. the names of the course
//    3. how many controls in each
//    4. how many runners in each
  var event,
      //headerTable,
      eventDescription,
      courseNameCells,
      courseLengthCells,
      courseNumberOfControlCells,
      courseName,
      courseLength,
      courseNumberOfControls,
      courseNumberOfRunners,
      courseNamePattern = /\s+\(\d+\)$/,  // to get the number of runners in the parenthised bit at the end
      runners,
      i;

  event = {};

  // note the source of this data
  if ( document.URL.indexOf( 'act.orienteering') !== -1) {
    event.source = 'Orienteering ACT';
  }
  else if ( document.URL.indexOf( 'websplits') !== -1) {
    event.source = 'WebSplits';
  }
  else {
    event.source = 'unknown';
  }

  eventDescription = document.querySelector( 'div#reporttop table tr td nobr').textContent;
  event.description = eventDescription ;

  courseNameCells            = document.querySelectorAll( 'td#c00');
  courseLengthCells          = document.querySelectorAll( 'td#c01');
  courseNumberOfControlCells = document.querySelectorAll( 'td#c02');

  event.courses = [];

  for ( i = 0; i < courseNameCells.length; i += 1) {
    event.courses[ i] = {};
    // course name field is <course_name> <(no_of_runners)>
    courseName = courseNameCells[ i].textContent;
    runners = courseNamePattern.exec( courseName);
    courseName = courseName.replace( runners[ 0], '');
    courseName = courseName.replace( /\s+/, '_');
    runners = /\d+/.exec( runners[ 0]);
    courseNumberOfRunners = runners[ 0];

    event.courses[ i].name = courseName;
    event.courses[ i].numberofrunners = courseNumberOfRunners;
    event.courses[ i].ageclasses = [];

    // take the Km off the end of the length
    courseLength = courseLengthCells[ i].textContent;
    courseLength = courseLength.replace( /\s+km/i, '');

    event.courses[ i].length = courseLength;

    // and take off the 'C' and the end of this field
    courseNumberOfControls= courseNumberOfControlCells[ i].textContent;
    courseNumberOfControls = courseNumberOfControls.replace( /\s+c/i, '');

    event.courses[ i].controls = courseNumberOfControls;
  }
  return event;
}

function getOACTResults( results) {

  results = getEventData();
  results = getResults( results);

  return results;
}

function getNewOACTCourse( ageclass, controls, allResults){
  /* see if this is the same as any other course */
  /* if not then give it a new one               */
  /* return the index of the course in allResults*/

  var thisSeq = controls.join( ''),
      otherSeq = '',
      thisCourse = '',
      nextCourse = 'course' + pad(( allResults.courses.length + 1).toString(), 2);

  for( let i = 0; i < allResults.courses.length; i++){
    otherSeq = allResults.courses[ i].controlcodes.join( '');
    if( thisSeq === otherSeq){
      allResults.courses[ i].ageclasses.push( ageclass);
      return [ i, allResults];
    }
  }

  if( thisCourse === ''){
    /* add a new course to results */
    let newCourse = {};
    newCourse.name = nextCourse;
    newCourse.ageclasses = [ ageclass];
    newCourse.controlcodes = controls;

    allResults.courses.push( newCourse);
  }

  return [ allResults.courses.length - 1, allResults];
}

function getNewOACTResults( allResults){
  /* get split results from new OACT web pages */

  var ageClasses = [];

  let eventTitle  = document.getElementsByTagName( 'h1')[0].textContent;
  let classHeaders = document.getElementsByTagName( 'h3');
  let splitsTables = document.getElementsByClassName( 'evt-results');

  /* set event title */
  allResults.description = eventTitle;

  /* initialise other results structures */
  allResults.courses = [];
  /* data structure
   allResults = {  description: '',
                   courses = [
                               {  name: string,
                                  controlcodes:[],
                                  ageclasses: [],
                                  runners:[
                                            {  name,
                                               club,
                                               time,
                                               splits:{ raw:[]}
                                             }
                                          ]
                               }
                            ]
                };
  */

  /* get the list of age classes from the h3 elements */
  for ( let i = 0; i < classHeaders.length; i++) {
    ageClasses[ i] = classHeaders[ i].textContent;
  }

  /* check whether we have the same number of age classes and result tables */
  if( ageClasses.length !== splitsTables.length) {
    alert( 'class and results length mismatch! ' + ageClasses.length + ' ' + results.length);
    return 0; /* exit here, no point in going further */
  }

  /* now work our way through the results which are shown by age class  */
  /* but we can get them into course by comparing control sequences     */
  for ( let i = 0; i < splitsTables.length; i ++) {

    /* set the age class since the two collections are in sync */
    thisAgeClass = ageClasses[ i];

    /* get the control sequence */
    let th = splitsTables[ i].getElementsByTagName( 'tr')[0].getElementsByTagName( 'th');
    let controls = [];
    for ( let j = 1; j < th.length; j++) {
       items = th[j].innerHTML.split( '<br>');
       if( items.length > 1){
         controls[ j - 1] = items[ 1];
       }
       else {
         controls[ j -1] = items[ 0];
      }
    }

    /* either add ageclass to an exisitng course or set up  a new course */
    var returnValues = getNewOACTCourse( thisAgeClass, controls, allResults);
    var courseIndex = returnValues[ 0];   // which course to add runners to
    allResults = returnValues[ 1];        // preserve changes made in the function

    /* now get the runners */
    if( allResults.courses[ courseIndex].runners === undefined){
      allResults.courses[ courseIndex].runners = [];
    }

    let runnerRows = splitsTables[ i].getElementsByClassName( 'classResult OK');

    for( let j = 0; j < runnerRows.length; j++){
      /* set up the runner */
      let thisRowInfo = runnerRows[ j].getElementsByTagName( 'th');
      let runner = {};
      runner.name = thisRowInfo[ 0].textContent;
      runner.club = thisRowInfo[ 1].textContent;
      runner.time = thisRowInfo[ 2].textContent;
      runner.ageclass = thisAgeClass;

      /* add the split times */
      runner.splits = {};
      let thisRowTimes = runnerRows[ j].getElementsByTagName( 'td');
      thisSplits = [];
      for( let k = 0; k < thisRowTimes.length; k++){
        let elementContents = thisRowTimes[ k].getElementsByClassName( 's')[0].textContent;
        sTime = elementContents.split( ' ')[ 0];
        thisSplits.push( sTime);
      }
      runner.splits.raw = thisSplits;

      allResults.courses[ courseIndex].runners.push( runner);
    }
  }
  //console.log( allResults);
  return allResults;
}

function getWMOCresults( results) {

  var message = [];
  var courseData = window.document.querySelectorAll( 'div.classinfo');
  var ageClass = stripWhitespace( courseData[ 0].querySelector( 'span.classheader').textContent);
  var eventTitle = courseData[ 0].textContent;
  var re = new RegExp( ageClass);
  eventTitle = stripWhitespace( eventTitle.replace( re, ''));

  // extract the splits data from the web page
  trs = window.document.querySelectorAll( 'tr');

  for ( let i = 0; i < trs.length; i += 1) {
    tds = trs[ i].querySelectorAll( 'td');
    messageRow = [];
    for ( j = 0; j < tds.length; j += 1) {
      messageRow.push( stripWhitespace( tds[ j].textContent));
    }
    message.push( messageRow);
  }

  results = processWMOCMessage( message, ageClass, eventTitle);

  return results;
}

// this is where we start
//var window = {};
window.onload = function() {
  var
    i,
    j,
    results = {},
    message = [],
    messageRow = [],
    courseData = [],
    eventTitle,
    ageClass,
    tds,
    trs,
    source = 'unknown',
    splitsButton,
    splitsButtonPlace,
    splitsStyleText = function() {/*
  font-family: Arial, Helvetica, sans-serif;
  font-size: 12px;
  color: #ffffff;
  padding: 10px 20px;
  background: -moz-linear-gradient(
    top,
    #42aaff 0%,
    #003366);
  background: -webkit-gradient(
    linear, left top, left bottom,
    from(#42aaff),
    to(#003366));
  -moz-border-radius: 10px;
  -webkit-border-radius: 10px;
  border-radius: 10px;
  border: 1px solid #003366;
  -moz-box-shadow:
    0px 1px 3px rgba(000,000,000,0.5),
    inset 0px 0px 1px rgba(255,255,255,0.5);
  -webkit-box-shadow:
    0px 1px 3px rgba(000,000,000,0.5),
    inset 0px 0px 1px rgba(255,255,255,0.5);
  box-shadow:
    0px 1px 3px rgba(000,000,000,0.5),
    inset 0px 0px 1px rgba(255,255,255,0.5);
  text-shadow:
    0px -1px 0px rgba(000,000,000,0.7),
    0px 1px 0px rgba(255,255,255,0.3);
  margin-bottom: 10px;
*/}.toString().slice( 15, -3);

  // determine where we are getting this data from
  // get source

  if ( document.URL.indexOf( 'winsplits') > 0) {
    source = 'Winsplits';
    // get the event and course / age class data
    courseData = window.frames[ 1].document.querySelectorAll( 'table.border table a.menubar');
    eventTitle = courseData[ 0].textContent;
    ageClass = courseData[ 1].textContent;

    // extract the splits data from the web page
    // these are in frame name 'main'
    trs = window.frames[ 2].document.querySelectorAll( 'tr');
    for ( i = 0; i < trs.length; i += 1) {
      tds = trs[ i].querySelectorAll( 'td');
      messageRow = [];
      for ( j = 0; j < tds.length; j += 1) {
        messageRow.push( tds[ j].textContent);
      }
      message.push( messageRow);
    }
    if ( validateMessage( message)) {
      results = processMessage( message, ageClass, eventTitle);
    }
    else {
      window.alert( 'Unusual Winsplit options selected. Only tick "position, finish times and extended information" at the foot of the page');
      return 0;
    }
    results.source = source;
    splitsButtonPlace = window.frames[ 2].document; // make the button
  }
  //else if ( document.URL.indexOf( 'act.orienteering.asn.au/gfolder/results') > 0) {
  //  results.source = 'Old Orienteering ACT';
  //  if ( document.querySelector( 'body div#reporttop tr:nth-child( 2) td nobr').textContent !== 'Split time results') {
  //    return 0;
  //  }
  //  results = getOACTResults( results);
  //  splitsButtonPlace = document; // make the button
  //}
  else if ( document.URL.indexOf( 'act.orienteering.asn.au/eventor/results/splits') > 0) {
    results.source = 'Orienteering ACT';
    results = getNewOACTResults( results);
    splitsButtonPlace = document; // make the button
  }
  else if ( document.URL.indexOf( 'wmoc') > 0) {
    results.source = 'WMOC';
    results = getWMOCresults( results);
    splitsButtonPlace = document; // make the button
  }
  else {
    window.alert( 'Fell: ' + document.URL);
    return 0;
  }

  // do the popup
  splitsButton = splitsButtonPlace.createElement( 'button');
  splitsButton.setAttribute( 'class', 'splits');
  splitsButton.setAttribute( 'style', splitsStyleText);
  splitsButton.id = 'splitsButton';
  splitsButton.textContent = 'Show Visualisation!';
  splitsButton.onclick = function(){ setupNewWindow( results);};
  splitsButtonPlace.body.insertBefore( splitsButton, splitsButtonPlace.body.firstChild);
};