nascent / TFL Delay Checker

// ==UserScript==
// @name         TFL Delay Checker
// @namespace    http://tampermonkey.net/
// @version      0.91
// @description  Check each journey in TFL Journey Planner to look for delays of 15 minutes or over.
// @author       You
// @match        https://contactless.tfl.gov.uk/NewStatements*
// @grant        GM_setValue
// @grant        GM_getValue
// @updateURL https://openuserjs.org/meta/nascent/TFL_Delay_Checker.meta.js
// @downloadURL    https://openuserjs.org/install/nascent/TFL_Delay_Checker.user.js
// @license GPL-3.0-or-later
// ==/UserScript==
'use strict';

// version 0.9 - Automatically collapse or expand all journey headers depending on setting.
// version 0.8 - Removed modal alert if expected refunds requested, now highlights rows red and changed the statement header message.
// version 0.7 - Stations and expected times are now queried on first run and stored in local settings.
// version 0.6 - Journey times and delay lengths are now displayed on web page.
// version 0.5 - Updated default times for new journey, added filter for specified journey stations.
// version 0.4 - Nested evaluate recode to correct get date from parent node.
// version 0.3 - Recode for TFL redesign
// version 0.2 - Fix for time occuring across midnight
// version 0.1 - initial release

//Todo:  ui to clear saved stations
//Todo:  support for multiple start and end stations

//Manually override stored settings (Station names need to read exactly as show in https://tfl.gov.uk/plan-a-journey)
//GM_setValue("tflStation1","Stratford International"); // eg: "Oxford Circus"
//GM_setValue("tflStation2","London St Pancras International"); // eg: "Mansion House"
//GM_setValue("expectedJourneyTime","23"); //in minutes

//GM_setValue("expandAllJourneys","true"); // automatically expand all journeys


//User settings varaiables (use the above GM_setValue block to make changes rather than here)
var expectedJourneyTime = 0;
var maxDelayTime = 15; //constant. tfl allow for 15 minutes worth of delays before allowing delay refunds
var journeyStation1 = "";
var journeyStation2 = "";
var autoExpandAllJourneys = false;

autoExpandAllJourneys = GM_getValue("expandAllJourneys",false);


//Automatically Collapse/Expand journeys
var journeys = document.getElementsByClassName("list-group collapse");
//alert ("collapsed " + journeys.length);
// Loop through the journeys array
Array.prototype.forEach.call(journeys, function(el) {
    if (autoExpandAllJourneys) {
        //Expand all
        if (el.className == "list-group collapse"){
            el.className = "list-group collapse in";
        }
    }
    else {
        //Collpase all
         if (el.className == "list-group collapse in"){
            el.className = "list-group collapse";
        }
     }
});

//Calculate journey times and alert on delays
expectedJourneyTime = GM_getValue("expectedJourneyTime");
if (expectedJourneyTime === undefined) {
    var input = prompt("What is the expected journey time of your commute (minutes)?", 20);
    if (input === null) {
        alert("Input must be valid");
        return;
    }
    else {
        expectedJourneyTime = input;
        GM_setValue("expectedJourneyTime",expectedJourneyTime);
    }
}

journeyStation1 = GM_getValue("tflStation1");
if (journeyStation1 === undefined) {
    input = prompt("What is station 1 of your commute?", "Oxford Circus");
    if (input === null) {
        alert("Input must be valid");
        return;
    }
    else {
        journeyStation1 = input;
        GM_setValue("tflStation1",journeyStation1);
    }
}
journeyStation2 = GM_getValue("tflStation2");
if (journeyStation2 === undefined) {
    input = prompt("What is station 2 of your commute?", "Mansion House");
    if (input === null) {
        alert("Input must be valid");
        return;
    }
    else {
        journeyStation2 = input;
        GM_setValue("tflStation2",journeyStation2);
    }
}

//Display settings
//alert("expectedJourneyTime: " + expectedJourneyTime);
//alert("journeyStation1: " + journeyStation1);
//alert("journeyStation2: " + journeyStation2);



var allDivs, thisDiv;
allDivs = document.evaluate(
    "//div[@class='statements-list clearfix']",
    document,
    null,
    XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
    null);

var delay = 0;
var anyRefundsNeeded = false;

if (allDivs) {
    console.debug("Total Divs: " +allDivs.snapshotLength);

    for (var i = 0; i < allDivs.snapshotLength; i++) {

        thisDiv = allDivs.snapshotItem(i).innerHTML;

        //window.prompt(i + ", thisDiv", thisDiv);

        var divArray = thisDiv.match(/class="date-link">(.+?)</); //Journey Divs





        var allRows, thisRow;
        allRows = document.evaluate(
            ".//div[@class='col-xs-9']",
            allDivs.snapshotItem(i),
            null,
            XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
            null);

        if (allRows) {

            for (var j = 0; j < allRows.snapshotLength; j++) {
                thisRow = allRows.snapshotItem(j).innerHTML;
                var rowArray = thisRow.match(/journey-from">(.+?)<[\s\S]+?journey-to">(.+?)<[\s\S]+?journey-time">(.+?)</); //Journey Divs
                if (rowArray) {
                    var res = rowArray[3].split(" - ");
                   var journeyTime;
                   journeyTime = CompareTimes(res[0], res[1]); // / 1000 / 60
                   allRows.snapshotItem(j).innerHTML += "<p style='color:black;font-size:10px'>Journey Length: " + journeyTime + "mins</p>";

                    //Is the journey between desired 2 stations
                    if ((journeyStation1 == rowArray[1] && journeyStation2 == rowArray[2]) || (journeyStation1 == rowArray[2] && journeyStation2 == rowArray[1])) {
                        allRows.snapshotItem(j).innerHTML += "<p style='color:black;font-size:10px'>Expected Time: " + expectedJourneyTime + "mins</p>";
                        delay = journeyTime - expectedJourneyTime;
                        if (delay > 0) {
                            allRows.snapshotItem(j).innerHTML += "<p style='color:black;font-size:10px'>Delay: " + delay + "mins</p>";
                        }
                        else {
                            allRows.snapshotItem(j).innerHTML += "<p style='color:black;font-size:10px'>No delay!</p>";
                        }
                        if (delay > maxDelayTime) { //Delay more than journeyTime (" + expectedJourneyTime + ") + " + maxDelayTime + " minutes delayTime
                            allRows.snapshotItem(j).innerHTML += "<p style='font-size:11px'><t style='background-color:red;;color:white;'>Refund needed!</t></p>";
                            var rootDiv = allRows.snapshotItem(j).parentNode.parentNode.parentNode.parentNode;
                            rootDiv.childNodes[1].setAttribute("style", "color: red !important");
                            rootDiv.childNodes[1].setAttribute("style", "background-color:red !important");
                            anyRefundsNeeded = true;
                        }
                        else {
                            allRows.snapshotItem(j).innerHTML += "<p style='color:black;font-size:11px'>No refund expected. "+journeyTime+"-"+expectedJourneyTime+"</p>";
                        }
                    }

                }
                else
                {
                    console.error("TFL Delay Checker: No rows found " + divArray.length + "\n " + thisRow);

                }
            }
        }
        else {
            console.error("Unable to evaluate rows");
        }
    }
    if (anyRefundsNeeded == true) {

        var headerNodes = document.getElementsByTagName("h2");
        for (var h = 0; h < headerNodes.length; h++) {
            if (headerNodes[h].innerText == "Statement"){
                headerNodes[h].innerText = "Some journeys require a refund";
                headerNodes[h].setAttribute("style", "color: red !important");
            }
        }
    }
}
else {
    console.error("Unable to evaluate divs");
}

function CompareTimes(startTime, endTime) { //07:56 - 08:39
    var startDate = new Date("January 1, 1970 " + startTime +":00");
    var endDate = new Date("January 1, 1970 " + endTime +":00");
    if (endDate < startDate) {
        endDate.setDate(endDate.getDate() + 1);
    }
    var timeDiff = Math.abs(startDate - endDate);

    var hh = Math.floor(timeDiff / 1000 / 60 / 60);
    if(hh < 10) {
        hh = '0' + hh;
    }
    timeDiff -= hh * 1000 * 60 * 60;
    var mm = Math.floor(timeDiff / 1000 / 60);
    if(mm < 10) {
        mm = '0' + mm;
    }
    timeDiff -= mm * 1000 * 60;
    var ss = Math.floor(timeDiff / 1000);
    if(ss < 10) {
        ss = '0' + ss;
    }

    return Math.floor(Math.abs(startDate - endDate) / 1000 / 60);
}

function parseTime(s) {
    var part = s.match(/(\d+):(\d+)(?: )?(am|pm)?/i);
    var hh = parseInt(part[1], 10);
    var mm = parseInt(part[2], 10);
    var ap = part[3] ? part[3].toUpperCase() : null;

    if (ap === "AM") {
        if (hh == 12) {
            hh = 0;
        }
    }
    if (ap === "PM") {
        if (hh != 12) {
            hh += 12;
        }
    }

    //alert(hh + " " + mm);
    return { hh: hh, mm: mm };
}