MVG Departures Dashboard.

This script creates an dashboard that shows upcoming departures from Munich's Public Transport Agency (MVG: Münchner Verkehrsgesellschaft) API.

Screenshot

Technologies Used: Python 3.X (mvg_api, tkinter)

Description

This app creates a GUI (Graphical User Interface) with dynamically generated data from Munich's Public Transport API. The user is prompted to (optionally) enter a station and the departures are generated for that station in a retro-UI. If nothing is entered, the dashboard will show the departures for Munich Central Station (Hauptbahnof) by default. The app also has an option to Refresh the departures, as needed.

Installation

Install the python modules listed above, and simply run: fetch_data.py Enter the station name (e.g.: Lehel, Münchener Freiheit) and hit enter to see the UI.

Code
fetch_data.py

import mvg_api
import tkinter as tk
from GUI import *

def formatDepartures(departures):
    departureT=[]
    for item in departures:
        bus=item['label']
        destination = item['destination']
        minutes = item['departureTimeMinutes']
        departureT.append([bus,destination,minutes])
    return departureT


# pdb.set_trace()
station = str(input("Enter your requested station:"))
station_id = mvg_api.get_id_for_station(station) #
# pdb.set_trace()
if station_id=='de:06412:10' or station_id=='' or station_id is None: 
    station_id = 'de:09162:6' #Haupybahnof station ID as default
    print("Station name not found, default to Hauptbahnof...")

rawData = mvg_api.get_departures(station_id)
departures=formatDepartures(rawData)


#Get data for a particular station and create GUI 
root = tk.Tk()
gui = GUI(root,station_id)
gui.populateTable(departures)
root.mainloop()
GUI.py:


import tkinter as tk
import mvg_api

onPi=True
sideColumnMinSize = 100
departureFontSize = 40
destinationFontSize = 40
headerFontSize = 25
subHeaderFontSize = 20
maxFutureDepartureTime = 120  # The maximum amount of time (in minutes) left for a departure that is displayed

class GUI:
    def __init__(self, master, station_id,**kwargs):
        self.master = master
        self.station_id = station_id
        
        # A list that will hold the temporary departure frames so to destroy them upon refreshing
        self.departureRowFrames = []
        self.currentlyDisplayedDepartures = []  # Used to decide whether to refresh the display

        self.master.grid()
        self.master.title("Departures")

        # A new frame inside master with boarder
        headerFrame = tk.Frame(master, bd="0")
        # Set the header frame's background to black
        headerFrame.configure(background='black')
        # Use the grid layout and expand the frame
        headerFrame.grid(row=0, sticky=tk.E + tk.W)

        # Label inside header frame
        headerLbl = tk.Label(headerFrame, text="Departures", font=("Helvetica bold", headerFontSize), bg="black", fg="white")
        # Place label on grid layout, row 0 and do not expand
        headerLbl.grid(row=0)
        # Center column 0 inside header frame
        headerFrame.grid_columnconfigure(0, weight=1)

        subHeadersFrame = tk.Frame(master)
        subHeadersFrame.configure(background='black')
        subHeadersFrame.grid(row=1, sticky=tk.E + tk.W)

        busNoLbl = tk.Label(subHeadersFrame, text="Line Number", font=("Courier", subHeaderFontSize), bg="black", fg="white")
        busDestLbl = tk.Label(subHeadersFrame, text="Destination", font=("Courier", subHeaderFontSize), bg="black", fg="white")
        minsLeftLbl = tk.Label(subHeadersFrame, text="Minutes Left", font=("Courier", subHeaderFontSize), bg="black", fg="white")

        busNoLbl.grid(row=0, column=0)
        busDestLbl.grid(row=0, column=1)
        minsLeftLbl.grid(row=0, column=2)
        # Expand the middle column so the other two move to the sides
        subHeadersFrame.grid_columnconfigure(1, weight=1)

        # The frame that will contain the departures
        departuresFrame = tk.Frame(master)
        departuresFrame.grid(row=2, sticky=tk.E + tk.W)
        departuresFrame.grid_columnconfigure(1, weight=1)
        self.departuresFrame = departuresFrame  # Class variable to hold the container frame for all the departures
        
        b = tk.Button(self.departuresFrame, text="Refresh",command=self.refreshDepartures).grid(row=13, column=2) #,command=self.refreshDepartures()
        q = tk.Button(self.departuresFrame, text="Exit",command=self.master.destroy).grid(row=14, column=2)


        # Keep everything in column 0 of master centered/expanded
        master.grid_columnconfigure(0, weight=1)

        # Make it full screen   
        # w, h = master.winfo_screenwidth(), master.winfo_screenheight()
        # master.overrideredirect(onPi)  # Set to 1 to force full window mode
        # master.geometry("%dx%d+0+0" % (w, h))
    
    def refreshDepartures(self):
        departures = mvg_api.get_departures(self.station_id)
        departureT=[]
        for item in departures:
            bus=item['label']
            destination = item['destination']
            minutes = item['departureTimeMinutes']
            departureT.append([bus,destination,minutes])
        self.populateTable(departureT)

    def populateTable(self, departures): 
        currentRow = 0
        for count,departure in enumerate(departures):
            (bus, destination, minutes) = departure
            # If a departure is too far in the future, don't display it
            if minutes > maxFutureDepartureTime:
                continue
            # Change background color for each row
            bgColor = "#FFF289" if currentRow % 2 else "white"

            # The frame that will contain each departure
            rowFrame = tk.Frame(self.departuresFrame)
            rowFrame.grid(row=currentRow, columnspan=3, sticky=tk.E + tk.W)
            rowFrame.configure(background=bgColor)
            currentRow += 1

            # After we have created the frame that will hold each departure, create the labels
            busNo = tk.Label(rowFrame, text=bus, font=("Helvetica", departureFontSize), bg=bgColor)
            busDest = tk.Label(rowFrame, text=destination, font=("Helvetica", destinationFontSize), bg=bgColor)
            minsLeft = tk.Label(rowFrame, text=int(minutes) if int(minutes) != 0 else "Now", font=("Helvetica", departureFontSize), bg=bgColor)
            busNo.grid(row=0, column=0)
            busDest.grid(row=0, column=1)
            minsLeft.grid(row=0, column=2)

            # Expand the middle column to push the other two to the sides
            rowFrame.grid_columnconfigure(1, weight=1)
            # Set the minimum size of the side columns so the middle column text is always at the same position
            rowFrame.grid_columnconfigure(0, minsize=sideColumnMinSize)
            rowFrame.grid_columnconfigure(2, minsize=sideColumnMinSize)

            # Add the newly created frame to a list so we can destroy it later when we refresh the departures
            self.departureRowFrames.append(rowFrame)
            
            if count==(len(departures)-20): #Normally returns 30 last departures, so show only 10 (30-20=10)
                # The frame that will contain each departure
                rowFrame = tk.Frame(self.departuresFrame)
                rowFrame.grid(row=currentRow, columnspan=3, sticky=tk.E + tk.W)
                rowFrame.configure(background='#FFA500')
                currentRow += 1
                busNo.grid(row=0, column=0)
                busDest.grid(row=0, column=1)
                minsLeft.grid(row=0, column=2)

                # Expand the middle column to push the other two to the sides
                rowFrame.grid_columnconfigure(1, weight=1)
                # Set the minimum size of the side columns so the middle column text is always at the same position
                rowFrame.grid_columnconfigure(0, minsize=sideColumnMinSize)
                rowFrame.grid_columnconfigure(2, minsize=sideColumnMinSize)

                # Add the newly created frame to a list so we can destroy it later when we refresh the departures
                self.departureRowFrames.append(rowFrame)
                break

    # Destroy any existing frames containing departures that already exist
    def resetDepartures(self):
        for frame in self.departureRowFrames:
            frame.destroy()
        # Empty the list as we have destroyed everything that was included in it
        self.departureRowFrames = []

                
        
def main():    
    root = tk.Tk()
    gui = GUI(root)
    root.mainloop()

if __name__ == "__main__":
    main()

Back to my projects →