London Tube status

Script to get London Tube status using a TFL-provided API.

/************
* Get status update for London Underground lines
*
* Although TFL generally require auth, they appear
* to allow use of the Status request without it.
*/

// 1. Get the data in a JSON object using 'await'
const url = "https://api.tfl.gov.uk/Line/Mode/tube/Status"
let req = new Request(url)
let json = await req.loadJSON()

// 2. Create a UITable to present the data.
let table = new UITable()

// 3. For each tube line ..
for (line of json) {

  // ... create a new row ...
  let row = new UITableRow()
  
  // ... get the line name and statuses ...
  let lineName = line.name
  let status = line.lineStatuses.map(s => s.statusSeverityDescription)
  
  // ... generate a string from statuses ...
  let count = status.length
  let str = status.reduce((t,s,i) => {
    t = t + s
    if (i < (count- 1) && count > 0) { t = t + " &\n "}
    return t
    }, "")
    
  // ... calculate row height ...
  let height = 16 + Math.max(count,2) * 22
  row.height = height
  
  // ... draw a colored rect
  const cW = 80
  let c = new DrawContext()
  c.size = new Size(cW, height * 4)
  c.setFillColor(getLineColor(lineName))
  c.fill(new Rect(0, 0 , cW, height * 4))
  
  // ... populate the row with data ...
  let imageCell = row.addImage(c.getImage())
  let titleCell = row.addText(lineName)
  let statusCell = row.addText(str)
  
  // ... format columns and rows ...
  row.cellSpacing = 10
  imageCell.widthWeight = 5
  titleCell.widthWeight = 35
  statusCell.widthWeight = 60
  
  // ... add row to table ...
  table.addRow(row)
}

// ... and present table
QuickLook.present(table)

function getLineColor(line) {
  switch(line) {
    case "Bakerloo":
      return new Color("#B26300")
      break
    case "Central":
      return new Color("#DC241F")
      break
    case "Circle":
      return new Color("#FFD329")
      break
    case "District":
      return new Color("#007D32")
      break
    case "Hammersmith & City":
      return new Color("#F4A9BE")
      break
    case "Jubilee":
      return new Color("#A1A5A7")
      break
    case "Metropolitan":
      return new Color("#9B0058")
      break
    case "Northern":
      return new Color("#000")
      break
    case "Piccadilly":
      return new Color("#0019A8")
      break
    case "Victoria":
      return new Color("#0098D8")
      break
    case "Waterloo & City":
      return new Color("#93CEBA")
      break
    default:
      return new Color("CCC")
      break
  }
}
7 Likes

Nice use case - I’ll be in and out of London a fair bit with work these next few months.

Quick question though, should there ever be a double status like this? It doesn’t seem quite right to me.

1 Like

The API returns an array of statuses (if that’s a word!) for each line - the actual detail behind the above is that there’s severe delays on part of the line (first status) and minor delays on the rest (second status). Haven’t had time yet to work out the best way to present that.

Thanks. I don’t travel through or around London a great deal. I have seen severe and good statuses before but no others that I recall - though there must surely have been at some point and I’ve simply not noted it!

I just checked my usual app for planning my routes and the line status it provides actually looks quite similarly displayed. They just opted to include a plus to indicate a combined status.

Thanks again for the script. I suspect it’ll be far faster to trigger than my usual app :sunglasses:

Ah, quire like the idea - have added an ampersand and indent on subsequent lines if there is more than one status on the line!

1 Like

I think they may have just right aligned that cell!

I’ll be in London next month and look forward to using this :grin:

Is there away to get the shape as Circle instead of Rectangle?

Further updated to look a bit nicer and to include details behind status messages:

/************
* Get status update for London Underground lines
*
* Although TFL generally require auth, they appear
* to allow use of the Status request without it.
*/

// 1. Get the data in a JSON object using 'await'
const url = "https://api.tfl.gov.uk/Line/Mode/tube/Status"
let req = new Request(url)
let json = await req.loadJSON()

// 2. Parse each line into required items
const data = []

for (line of json) {

  // ... create a new item ...
  let dataItem = {}
  
  // ... get the line name and store in <h1> ...
  dataItem.name = line.name
  dataItem.displayName = `<h2>${line.name}</h2>`
  
  // ... get status...
  let status = line.lineStatuses.map(s => [s.statusSeverityDescription, s.reason] )
  
  // ... and generate <h1> and <p> tags ...
  let count = status.length
  let str = status.reduce((t,s) => {
    t = t + `<h3>${s[0]}</h3>`
    if (s[1]) { 
      let str2 = s[1]
      let strSearch = `${dataItem.name} Line: `
      if (str2.startsWith(strSearch)) {
        str2 = str2.substring(strSearch.length)
      }
      
      t = t + `<p>${str2}</p>` 
    }
    return t
    }, "")
    
  dataItem.displayStatus = str
  data.push(dataItem)
}

// 3. Generare combined html

const FONTSIZE = 14;
const COLORWIDTH = 10;
const NAMEWIDTH = FONTSIZE * 10;
const MARGIN = 5;

let headContent = `
  <meta name="viewport" content="initial-scale=1.0">
  <style>
    .body, h1, h2, h3, p {
      font-family: -apple-system, sans-serif;
    }
    body {
      display: grid;
      grid-template-columns: (${COLORWIDTH}px, ${NAMEWIDTH}px, 1fr);
      grid-row-gap: ${MARGIN}px;
      grid-column-gap: ${MARGIN}px
    }
    h1 {
      font-size: ${FONTSIZE * 1.6}px;
      font-weight: 600;
      margin-top: 0;
      grid-column: 1/4;
    }
    h2 {
      font-size: ${FONTSIZE *1.5}px;
      font-weight: 400;
      margin: 0;
    }
    h3 {
      font-size: ${FONTSIZE *1.3}px;
      margin: 0;
    }
    p {
      margin-top: 0;
      margin-bottom: 0.25em;
    }
    .color {
      width: ${COLORWIDTH}px;
      grid-column: 1;
    }
    .name {
      display: inline-block;
      width: ${NAMEWIDTH}px;
      grid-column: 2;
    }
    .status {
      display: inline;
      grid-column: 3;
    }
    .spacer {
      grid-column: 1/4;
      border-top: 1px solid gray;
    }
  </style>
`

let bodyContent = data.reduce((t,s) => {
  const c = getLineColor(s.name)
  t = t + '<div class="spacer"></div>'
  t = t + `<div class="color" style="background-color: ${c}"></div>`
  t = t + `<div class="name">${s.displayName}</div>`
  t = t + `<div class="status">${s.displayStatus}</div>`
  return t
}, "")

let date = new Date()
let dateStr = date.getHours() + ":" + date.getMinutes()

let myHTML = `
<html>
   <head>
    ${headContent}
  </head>
  <body>
    <h1>London Tube Status @ ${dateStr}</h1>
    ${bodyContent}
    <div class="spacer"></div>
  </body>
</html>
`

WebView.loadHTML(myHTML)

// not sure why I've done this as a function, would be better as an object with the line name as a key?

function getLineColor(line) {
  switch(line) {
    case "Bakerloo":
      return "#B26300"
      break
    case "Central":
      return "#DC241F"
      break
    case "Circle":
      return "#FFD329"
      break
    case "District":
      return "#007D32"
      break
    case "Hammersmith & City":
      return "#F4A9BE"
      break
    case "Jubilee":
      return "#A1A5A7"
      break
    case "Metropolitan":
      return "#9B0058"
      break
    case "Northern":
      return "#000"
      break
    case "Piccadilly":
      return "#0019A8"
      break
    case "Victoria":
      return "#0098D8"
      break
    case "Waterloo & City":
      return "#93CEBA"
      break
    default:
      return "CCC"
      break
  }
}

(Nothing like answering promptly, I know!)

Not sure about previous code, but with revised code they’re a css-styled div, so I imagine that setting a height property on the div (equal to width) and adding a border-radius: 50% style in the css would achieve this.