virtual-metro/virtual_metro/__init__.py

238 lines
8.3 KiB
Python
Executable File

#!/usr/bin/env python3
import flask
app = flask.Flask(__name__)
from . import config
import hashlib
import hmac
import json
import pytz
import traceback
from binascii import hexlify
from datetime import datetime, timedelta
from urllib.parse import urlencode
from urllib.request import Request, urlopen
request_cache = {}
def do_request(endpoint, args=None):
url = endpoint + '?devid=' + config.PTV_USER_ID
if args:
url += '&' + urlencode(args)
# Check cache
timenow = pytz.utc.localize(datetime.utcnow()).astimezone(timezone)
cachexp = timenow - timedelta(seconds=60)
if url in request_cache:
if request_cache[url][0] >= cachexp:
# Use cached response
return request_cache[url][1]
# Generate signature
signature = hexlify(hmac.digest(config.PTV_API_KEY.encode('ascii'), url.encode('ascii'), 'sha1')).decode('ascii')
req = Request('https://timetableapi.ptv.vic.gov.au' + url + '&signature=' + signature, headers={'User-Agent': 'virtual-metro/0.1'})
try:
resp = urlopen(req)
except Exception as ex:
print('Unable to refresh cache')
traceback.print_exc()
if url in request_cache:
return request_cache[url][1]
else:
return None
data = json.load(resp)
# Cache the response
request_cache[url] = (timenow, data)
return data
def parse_date(dtstring):
return pytz.utc.localize(datetime.strptime(dtstring, '%Y-%m-%dT%H:%M:%SZ')).astimezone(timezone)
ROUTE_TYPE = 0
LOOP_STATIONS = ['Parliament', 'Melbourne Central', 'Flagstaff', 'Southern Cross', 'Flinders Street']
timezone = pytz.timezone('Australia/Melbourne')
@app.route('/')
@app.route('/index')
def index():
return flask.render_template('index.html')
def stop_to_name(stop, route_id):
name = stop['stop_name']
if name.endswith(' Station'):
name = name[:-8]
if name == 'Flemington Racecourse' and route_id and route_id != 16 and route_id != 17 and route_id != 1482:
# PTV bug??
name = 'South Kensington'
return name
route_stops = {} # Cache lookup
def parse_departure(departure, departures, timenow):
result = {}
result['dest'] = departures['runs'][str(departure['run_id'])]['destination_name']
result['sch'] = parse_date(departure['scheduled_departure_utc']).strftime('%I:%M').lstrip('0')
mins = (parse_date(departure['estimated_departure_utc'] or departure['scheduled_departure_utc']) - timenow).total_seconds() / 60
if mins < 0.5:
result['now'] = 'NOW'
else:
result['min'] = round(mins)
# Get stopping pattern
result['stops'] = []
pattern = do_request('/v3/pattern/run/{}/route_type/{}'.format(departure['run_id'], ROUTE_TYPE), {'expand': 'all'})
pattern_stops = [(x['stop_id'], stop_to_name(pattern['stops'][str(x['stop_id'])], departure['route_id']), False) for x in pattern['departures']]
# Get all stops on route
if (departure['route_id'], departure['direction_id']) not in route_stops:
stops = do_request('/v3/stops/route/{}/route_type/{}'.format(departure['route_id'], ROUTE_TYPE), {'direction_id': departure['direction_id']})
stops['stops'].sort(key=lambda x: x['stop_sequence'])
route_stops[(departure['route_id'], departure['direction_id'])] = stops['stops']
route_stops_dir = []
for stop in route_stops[(departure['route_id'], departure['direction_id'])]:
# Cut off at Flinders Street
route_stops_dir.append(stop)
if stop_to_name(stop, departure['route_id']) == 'Flinders Street':
break
# Calculate stopping pattern
result['stops'] = []
# Find this run along pattern_stops
ps_route_start = next((k for k, stop in enumerate(pattern_stops) if stop[0] == int(flask.request.args['stop_id'])), 0)
ps_route_end = next((k for k, stop in enumerate(pattern_stops) if stop[0] == pattern['runs'][str(departure['run_id'])]['final_stop_id']), len(pattern_stops)-1)
ps_route_flinders = next((k for k, stop in enumerate(pattern_stops) if stop[0] == 1071), len(pattern_stops)-1)
if ps_route_flinders > ps_route_start:
ps_route_end = min(ps_route_end, ps_route_flinders) # Cut off at Flinders Street
# Find this run along route_stops_dir
rsd_route_start = next((k for k, stop in enumerate(route_stops_dir) if stop['stop_id'] == int(flask.request.args['stop_id'])), 0)
rsd_route_end = next((k for k, stop in enumerate(route_stops_dir) if stop['stop_id'] == pattern['runs'][str(departure['run_id'])]['final_stop_id']), len(route_stops_dir)-1)
rsd_route_flinders = next((k for k, stop in enumerate(route_stops_dir) if stop['stop_id'] == 1071), len(route_stops_dir)-1)
if rsd_route_flinders > rsd_route_start:
rsd_route_end = min(rsd_route_end, rsd_route_flinders)
# Add express stops
for k, stop in enumerate(route_stops_dir[rsd_route_start:rsd_route_end+1]):
if any(x[0] == stop['stop_id'] for x in pattern_stops):
continue
if stop_to_name(stop, departure['route_id']) in LOOP_STATIONS:
continue
# Express stop
# Identify location based on previous stop
if rsd_route_start+k == 0:
ps_index = 0
else:
ps_index = next((l+1 for l, stop in enumerate(pattern_stops) if stop[0] == route_stops_dir[rsd_route_start+k-1]['stop_id']), None)
if ps_index is None:
continue
pattern_stops.insert(ps_index, (stop['stop_id'], stop_to_name(stop, departure['route_id']), True))
ps_route_end += 1
# Convert to string
express_stops = []
num_city_loop = 0
for _, stop_name, is_express in pattern_stops[ps_route_start+1:ps_route_end+1]:
if is_express:
result['stops'].append(' ---')
express_stops.append(stop_name)
else:
result['stops'].append(stop_name)
if stop_name in LOOP_STATIONS:
num_city_loop += 1
# Impute remainder of journey
if result['stops'] and result['stops'][-1] == 'Parliament':
result['stops'].append('Melbourne Central')
result['stops'].append('Flagstaff')
result['stops'].append('Southern Cross')
result['stops'].append('Flinders Street')
num_city_loop += 4
result['dest'] = result['stops'][-1] if result['stops'] else None
if result['dest'] is None:
result['dest'] = 'Arrival'
result['desc'] = ''
elif result['dest'] in LOOP_STATIONS:
# Up train
# Is this a City Loop train?
if num_city_loop >= 3:
result['dest'] = 'City Loop'
result['desc'] = 'All Except {}'.format(express_stops[0]) if len(express_stops) == 1 else 'Limited Express' if len(express_stops) > 1 else 'Stops All Stations'
else:
# Down train
# Is this a via City Loop train?
if num_city_loop >= 3:
result['desc'] = 'Ltd Express via City Loop' if len(express_stops) > 1 else 'Stops All via City Loop'
else:
result['desc'] = 'All Except {}'.format(express_stops[0]) if len(express_stops) == 1 else 'Limited Express' if len(express_stops) > 1 else 'Stops All Stations'
return result
@app.route('/latest')
def latest():
timenow = pytz.utc.localize(datetime.utcnow()).astimezone(timezone)
result = {}
result['time_offset'] = timenow.utcoffset().total_seconds()
departures = do_request('/v3/departures/route_type/{}/stop/{}'.format(ROUTE_TYPE, flask.request.args['stop_id']), {'platform_numbers': flask.request.args['plat_id'], 'max_results': '5', 'expand': 'all'})
departures['departures'].sort(key=lambda x: x['scheduled_departure_utc'])
if len(departures['departures']) == 0:
# Invalid stop ID, platform ID, no departures, etc.
return flask.jsonify(result)
result['stop_name'] = stop_to_name(departures['stops'][flask.request.args['stop_id']], None)
# Next train
for i, departure in enumerate(departures['departures']):
if parse_date(departure['estimated_departure_utc'] or departure['scheduled_departure_utc']) < timenow:
continue
# This is the next train
result.update(parse_departure(departure, departures, timenow))
break
# Next trains
result['next'] = []
for departure in departures['departures'][i+1:i+3]:
result['next'].append(parse_departure(departure, departures, timenow))
return flask.jsonify(result)
# Cache list of stations
stns = None
def get_station_list():
global stns
if not stns:
stn_list = {}
routes = do_request('/v3/routes', {'route_types': ROUTE_TYPE})
for route in routes['routes']:
stops = do_request('/v3/stops/route/{}/route_type/{}'.format(route['route_id'], ROUTE_TYPE), {})
for stop in stops['stops']:
stn_list[stop['stop_id']] = stop['stop_name'].replace(' Station', '')
stns = [(k, v) for k, v in stn_list.items()]
stns.sort(key=lambda x: x[1])
if not app.debug:
get_station_list()
@app.route('/stations')
def stations():
get_station_list()
return flask.render_template('stations.html', stations=stns)