From ab5ee7de04ded6c23c7847f3cfc353bd9490d0d1 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Mon, 31 Dec 2018 20:26:36 +1100 Subject: [PATCH] Initial commit --- .gitignore | 3 + virtual_metro/__init__.py | 115 +++++++ virtual_metro/static/index.html | 45 +++ virtual_metro/static/template.svg | 545 ++++++++++++++++++++++++++++++ 4 files changed, 708 insertions(+) create mode 100644 .gitignore create mode 100755 virtual_metro/__init__.py create mode 100644 virtual_metro/static/index.html create mode 100644 virtual_metro/static/template.svg diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4056689 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +__pycache__ +test.py +config.py diff --git a/virtual_metro/__init__.py b/virtual_metro/__init__.py new file mode 100755 index 0000000..63c33ca --- /dev/null +++ b/virtual_metro/__init__.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 + +import flask +app = flask.Flask(__name__) + +from . import config + +import hashlib +import hmac +import json +import pytz +from binascii import hexlify +from datetime import datetime +from urllib.parse import urlencode +from urllib.request import Request, urlopen + +def do_request(endpoint, args=None): + url = endpoint + '?devid=' + config.PTV_USER_ID + if args: + url += '&' + urlencode(args) + + # 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'}) + resp = urlopen(req) + return json.load(resp) + +def dest_to_service_name(dest): + if dest == 'Parliament': + return 'City Loop' + return dest + +def parse_date(dtstring): + return pytz.utc.localize(datetime.strptime(dtstring, '%Y-%m-%dT%H:%M:%SZ')).astimezone(timezone) + +STOP_ID = 1099 # Huntingdale Station +PLAT_ID = 1 +ROUTE_TYPE = 0 + +timezone = pytz.timezone('Australia/Melbourne') + +@app.route('/') +def index(): + return app.send_static_file('index.html') + +@app.route('/latest') +def latest(): + timenow = pytz.utc.localize(datetime.utcnow()).astimezone(timezone) + result = {} + + departures = do_request('/v3/departures/route_type/{}/stop/{}'.format(ROUTE_TYPE, STOP_ID), {'platform_numbers': str(PLAT_ID), 'max_results': '5', 'expand': 'all'}) + departures['departures'].sort(key=lambda x: x['scheduled_departure_utc']) + + # 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['dest'] = dest_to_service_name(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 = set(x['stop_id'] for x in pattern['departures']) + + # Get all stops on route + 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']) + + # Calculate stopping pattern until city loop + for j, stop in enumerate(stops['stops']): + if stop['stop_id'] == STOP_ID: + break + for stop in stops['stops'][j+1:]: + if stop['stop_id'] in pattern_stops: + result['stops'].append(stop['stop_name'].replace('Station', '').strip()) + else: + result['stops'].append('---') + if stop['stop_id'] == departures['runs'][str(departure['run_id'])]['final_stop_id']: + break + if stop['stop_id'] == 1155 or stop['stop_id'] == 1120 or stop['stop_id'] == 1068 or stop['stop_id'] == 1181 or stop['stop_id'] == 1071: # Parliament, MCS, Flagstaff, SXS, Flinders St + break + + # Calculate stopping pattern in city loop + pattern['departures'].sort(key=lambda x: x['scheduled_departure_utc']) + for k, stop2 in enumerate(pattern['departures']): + if stop2['stop_id'] == stop['stop_id']: + break + for stop in pattern['departures'][k+1:]: + result['stops'].append(pattern['stops'][str(stop['stop_id'])]['stop_name'].replace('Station', '').strip()) + + break + + # Next trains + + result['next'] = [] + for departure in departures['departures'][i+1:i+3]: + result['next'].append({}) + result['next'][-1]['dest'] = dest_to_service_name(departures['runs'][str(departure['run_id'])]['destination_name']) + result['next'][-1]['desc'] = 'Express' if departures['runs'][str(departure['run_id'])]['express_stop_count'] > 4 else 'Limited Express' + result['next'][-1]['sch'] = parse_date(departure['scheduled_departure_utc']).strftime('%I:%M').lstrip('0') + result['next'][-1]['min'] = '{} mins'.format(round((parse_date(departure['estimated_departure_utc'] or departure['scheduled_departure_utc']) - timenow).total_seconds() / 60)) + + return flask.jsonify(result) + +... diff --git a/virtual_metro/static/index.html b/virtual_metro/static/index.html new file mode 100644 index 0000000..5e660b4 --- /dev/null +++ b/virtual_metro/static/index.html @@ -0,0 +1,45 @@ + + + + + + + + + + + + + diff --git a/virtual_metro/static/template.svg b/virtual_metro/static/template.svg new file mode 100644 index 0000000..6c9a675 --- /dev/null +++ b/virtual_metro/static/template.svg @@ -0,0 +1,545 @@ + + + + + + + + + + image/svg+xml + + + + + + + + {{dest}} + + {{sch}} + {% if min %}mins{% endif %} + Departing + Schedule + {{stops[0]}} + {{stops[1]}} + {{stops[2]}} + {{stops[3]}} + {{stops[4]}} + {{stops[5]}} + {{stops[6]}} + + + Schedule + Train To + Departing + {{next[0].sch}} + {{next[1].sch}} + {{next[0].dest}} + {{next[1].dest}} + {{next[0].desc}} + {{next[1].desc}} + {{next[0].min}} + {{next[1].min}} + + + + {{stops[7]}} + {{stops[8]}} + {{stops[9]}} + {{stops[10]}} + {{stops[11]}} + {{stops[12]}} + {{stops[13]}} + {{stops[14]}} + {{stops[15]}} + {{stops[16]}} + {{stops[17]}} + {{stops[18]}} + {{stops[19]}} + {{stops[20]}} + {{min}} + {{time}} + {{now}} + +