Created
March 15, 2025 14:33
-
-
Save BXYMartin/93927ea6a99902d44c76edab0f99af1f to your computer and use it in GitHub Desktop.
Scriptable widget to monitor Transport for London (TFL) status
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const API_URL = "https://api.tfl.gov.uk/line/mode/tube/status"; | |
| // Define official TfL colors | |
| const LINE_COLORS = { | |
| "Bakerloo": "#B36305", | |
| "Central": "#E32017", | |
| "Circle": "#FFD300", | |
| "District": "#00782A", | |
| "Hammersmith & City": "#F3A9BB", | |
| "Jubilee": "#A0A5A9", | |
| "Metropolitan": "#9B0056", | |
| "Northern": "#000000", | |
| "Piccadilly": "#003688", | |
| "Victoria": "#0098D4", | |
| "Waterloo & City": "#95CDBA" | |
| }; | |
| // Define status severity colors | |
| const STATUS_COLORS = { | |
| 10: Color.green(), // Good Service | |
| 9: Color.yellow(), // Minor Delays | |
| 8: Color.yellow(), // Bus Service | |
| 7: Color.yellow(), // Reduced Service | |
| 6: Color.red(), // Severe Delays | |
| 5: Color.orange(), // Part Closure | |
| 4: Color.red(), // Planned Closure | |
| 3: Color.red(), // Suspended | |
| 2: Color.red(), // Service Closed | |
| 1: Color.red(), // Closed | |
| 0: Color.red(), // Special Service | |
| }; | |
| // Fetch Tube status from TfL API | |
| async function fetchTubeStatus() { | |
| try { | |
| let req = new Request(API_URL); | |
| let data = await req.loadJSON(); | |
| let tubeStatus = data.map(line => ({ | |
| name: line.name, | |
| status: line.lineStatuses[0]?.statusSeverityDescription || "Unknown", | |
| severity: line.lineStatuses[0]?.statusSeverity || 10, | |
| color: LINE_COLORS[line.name] || "#AAAAAA" | |
| })); | |
| // Prioritize Central and Piccadilly lines first, then sort by severity | |
| tubeStatus.sort((a, b) => { | |
| if (a.name === "Central" || a.name === "Piccadilly") return -1; | |
| if (b.name === "Central" || b.name === "Piccadilly") return 1; | |
| return a.severity - b.severity; // Higher severity first | |
| }); | |
| return tubeStatus; | |
| } catch (error) { | |
| console.error("Error fetching TfL data:", error); | |
| return []; | |
| } | |
| } | |
| // Create a capsule-styled label with a rectangle at the bottom | |
| function createCapsule(stack, text, bgColor, textColor = Color.white(), bottomColor = null) { | |
| let capsule = stack.addStack(); | |
| capsule.layoutHorizontally(); // Ensure vertical stacking | |
| capsule.setPadding(3, 6, 3, 6); | |
| capsule.backgroundColor = bgColor; | |
| capsule.cornerRadius = 10; | |
| capsule.size = new Size(60, 0); | |
| // If bottomColor is provided, add a rectangle at the bottom | |
| if (bottomColor) { | |
| // Create a hard transition gradient | |
| let gradient = new LinearGradient(); | |
| gradient.colors = [bgColor, bgColor, bottomColor, bottomColor, bgColor, bgColor]; // Hard transition | |
| gradient.locations = [0.0, 0.05, 0.051, 0.1, 0.11, 1.0]; // Sharp color change in the middle | |
| gradient.startPoint = new Point(0, 0); | |
| gradient.endPoint = new Point(1, 1); | |
| capsule.backgroundGradient = gradient; | |
| } | |
| let label = capsule.addText(text); | |
| label.font = Font.boldSystemFont(8); | |
| label.textColor = textColor; | |
| label.lineLimit = 1; | |
| label.minimumScaleFactor = 1.0; | |
| } | |
| function createText(stack, text, textColor = Color.white()) { | |
| let label = stack.addText(text); | |
| label.font = Font.boldSystemFont(10); | |
| label.textColor = textColor; | |
| label.lineLimit = 1; | |
| label.minimumScaleFactor = 1.0; | |
| } | |
| // Build widget UI | |
| async function createWidget(size) { | |
| let tubeStatus = await fetchTubeStatus(); | |
| let widget = new ListWidget(); | |
| widget.setPadding(18, 18, 18, 18); | |
| widget.backgroundColor = new Color("#323E48"); // Official TfL blue | |
| // Widget title | |
| widget.addSpacer(); | |
| headerStack = widget.addStack(); | |
| headerStack.layoutHorizontally(); | |
| let title = headerStack.addText(size == "small" ? "Status" : "London Tubes Status"); | |
| title.font = Font.boldSystemFont(12); | |
| title.textColor = Color.white(); | |
| headerStack.addSpacer(); | |
| let footer = headerStack.addText(new Date().toLocaleTimeString()); | |
| footer.rightAlignText(); | |
| footer.font = Font.systemFont(12); | |
| footer.textColor = Color.white(); | |
| footer.textOpacity = 0.8; | |
| widget.addSpacer(); | |
| // Define layout based on widget size | |
| let maxLines = size == "small" ? 10 : config.widgetFamily == "medium" ? 10 : 16; | |
| let displayedLines = tubeStatus.slice(0, maxLines); | |
| let columnStack = widget.addStack(); | |
| columnStack.layoutHorizontally(); | |
| let leftColumn = columnStack.addStack(); | |
| leftColumn.layoutVertically(); | |
| columnStack.addSpacer(size == "small" ?5 : 10); | |
| let rightColumn = columnStack.addStack(); | |
| rightColumn.layoutVertically(); | |
| displayedLines.forEach((line, index) => { | |
| let parentStack = index % 2 === 0 ? leftColumn : rightColumn; | |
| let row = parentStack.addStack(); | |
| row.layoutHorizontally(); | |
| row.centerAlignContent(); | |
| // Line name capsule | |
| if (size == "small") { | |
| createCapsule(row, line.name, STATUS_COLORS[line.severity] || Color.white(), Color.white(), new Color(line.color)); | |
| } | |
| else { | |
| createCapsule(row, line.name, new Color(line.color)); | |
| row.addSpacer(); | |
| // Status capsule with severity color | |
| createText(row, line.status, STATUS_COLORS[line.severity] || Color.white()); | |
| } | |
| parentStack.addSpacer(5); | |
| }); | |
| // Refresh timestamp | |
| widget.addSpacer(); | |
| return widget; | |
| } | |
| // Determine widget size | |
| let widget = await createWidget(config.widgetFamily || "small"); | |
| if (!config.runsInWidget) { | |
| await widget.presentSmall(); | |
| // await widget.presentMedium(); | |
| } | |
| const now = new Date(); | |
| const nextUpdate = new Date( | |
| now.getFullYear(), | |
| now.getMonth(), | |
| now.getDate(), | |
| now.getHours(), | |
| Math.ceil(now.getMinutes() / 30) * 30, | |
| 5, | |
| 0 | |
| ); | |
| widget.refreshAfterDate = nextUpdate; | |
| Script.setWidget(widget); | |
| Script.complete(); |
Author
BXYMartin
commented
Mar 15, 2025


Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment