In [1]:
from pathlib import Path
import json
from functools import reduce
import math
import datetime as dt
import pytz 
from itertools import product
from collections import OrderedDict
import time
import re
import sys

import requests
import numpy as np
import pandas as pd
import geopandas as gpd
import shapely.ops as so

import helpers as hp

%load_ext autoreload
%autoreload 2

Use Google Maps to compute commute matrices


In [ ]:
GOOGLE_MATRIX_URL = "https://maps.googleapis.com/maps/api/distancematrix/json"
GOOGLE_KEY = hp.get_secret('GOOGLE_API_KEY_ALEX')

def get_matrix(origins, destinations, mode, departure_time=None, url=GOOGLE_MATRIX_URL, key=GOOGLE_KEY, 
  timezone='Pacific/Auckland'):
    """
    Call Google to compute the duration-distance matrix from the list of origins to the list of destinations
    by the given mode at the given departure time.
    
    INPUT:
        origins
            List of WGS84 longitude-latitude pairs that will be round to 5 decimal places 
        destinations
            List of WGS84 longitude-latitude pairs that will be round to 5 decimal places 
        mode
            String; one of 'driving', 'walking', 'bicycling', or 'transit'
        departure_time
            Optional; ISO 8601 datetime string or 'now'; e.g. '2017-06-01T07:30:00'; can't be from the past
        timezone
            String; timezone for query, e.g. 'Pacific/Auckland'; see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones 
            
    OUTPUT:
        A decoded JSON string described at https://developers.google.com/maps/documentation/distance-matrix/intro#DirectionsResponseElements .
    NOTES:
        - If ``departure_time`` is not null and ``mode='driving'``, then each request must contain at most 100 elements, 
          where the number of elements equals the product of the number of origins and number of destinations.
    """
    valid_modes = ['driving', 'walking', 'bicycling', 'transit']
    if mode not in valid_modes:
        raise ValueError('mode must be one of {!s}'.format(valid_modes))
    
    origs = '|'.join(["{:.05f},{:.05f}".format(lat, lon) for lon, lat in origins])
    dests = '|'.join(["{:.05f},{:.05f}".format(lat, lon) for lon, lat in destinations])
    if departure_time not in [None, 'now']:
        tz = pytz.timezone(timezone)
        departure_time = dt.datetime.strptime(departure_time, '%Y-%m-%dT%H:%M:%S')
        departure_time = int(tz.localize(departure_time).timestamp())
        
    params = {
        'origins': origs,
        'destinations': dests,
        'key': key,
        'mode': mode,
        'departure_time': departure_time,
    }
    r = requests.get(url, params=params)

    # Raise an error if bad request
    r.raise_for_status()

    return r.json()         

def matrix_to_df(matrix, orig_names=None, dest_names=None):
    """
    Given a (decoded) JSON time-distance matrix of the form output by :func:``get_matrix``, 
    a list of origin names (defaults to [0, 1, 2, etc.]), 
    and a list of destination names (defaults to [0, 1, 2, etc.]), convert the matrix to a DataFrame with
    the columns:
    
    - ``'origin'``: one of ``orig_names``
    - ``'destination'``: one of ``dest_names``
    - ``'duration'``: time from origin to destination
    - ``'distance'``: distance from origin to destination
    
    The origin and destination names should be listed in the same order as the 'sources' and 'targets' 
    attributes of ``matrix``, respectively.
    """
    # Initialize DataFrame
    columns = ['orig', 'orig_name', 'dest', 'dest_name', 'duration', 'distance']
    f = pd.DataFrame([], columns=columns)
    
    # Append origins and destinations
    origs, dests =  zip(*product(matrix['origin_addresses'], matrix['destination_addresses']))
    f['orig'] = origs
    f['dest'] = dests
    if orig_names is not None and dest_names is not None:
        orig_names, dest_names = zip(*product(orig_names, dest_names))
        f['orig_name'] = orig_names
        f['dest_name'] = dest_names
        
    # Append durations and distances
    if 'duration_in_traffic' in matrix['rows'][0]['elements'][0]:
        dur_key = 'duration_in_traffic'
    else:
        dur_key = 'duration'
    durs = []
    dists = []
    for r in matrix['rows']:
        for e in r['elements']:
            if e['status'] == 'OK':
                durs.append(e[dur_key]['value'])
                dists.append(e['distance']['value'])
            else:
                durs.append(None)
                dists.append(None)
    f['duration'] = durs
    f['distance'] = dists

    return f

def build_matrix(rental_area_points, mode, departure_time=None, chunk_size=100, 
  url=GOOGLE_MATRIX_URL, key=GOOGLE_KEY):
    """
    Compute the duration-distance matrix between all pairs of rental area points given,
    but skip the diagonal entries, that is, the ones with origin equal to destination.
    To do this, call:func:`get_matrix` repeatedly.
    Group the duration-distance calls into ``chunk_size``-to-1 chunks. 
    
    INPUT:
        rental_area_points
            GeoDataFrame
        mode
            See :func:`get_matrix`
        departure_time
            See :func:`get_matrix`
        chunk_size
            Max number of origin-destination rows per matrix query
        url
            See :func:`get_matrix`
        key
            See :func:`get_matrix`
            
    OUTPUT:
        A DataFrame of the form...
        
    NOTES:
        - Sleeps for 1 second after every call to :func:`get_matrix` to stay within API usage limits
    """
    f = rental_area_points.copy()
    frames = []
    status = 'OK'
    for __, row in f.iterrows():
        # Quit if bad status
        if status != 'OK':
            print('Quitting because of bad status:', status)
            break
            
        # Set the single destination
        dests = [row['geometry'].coords[0]]  
        ra = row['rental_area']
        dest_names = [ra]
        
        # Create origin chunks and compute matrix for each chunk to destination 
        ff = f[f['rental_area'] != ra].copy()
        num_chunks = math.ceil(ff.shape[0]/chunk_size)
        for g in np.array_split(ff, num_chunks):
            # Get origins
            origs = [geo.coords[0] for geo in g['geometry']] 
            orig_names = g['rental_area'].values 
            # Get matrix
            try:
                j = get_matrix(origs, dests, mode=mode, departure_time=departure_time, url=url, key=key)
                status = j['status']
                if status != 'OK':
                    break
                df = matrix_to_df(j, orig_names, dest_names)
            except:
                df = pd.DataFrame()
                df['orig'] = np.nan
                df['orig_name'] = orig_names
                df['dest'] = np.nan
                df['dest_name'] = ra
                df['duration'] = np.nan
                df['distance'] = np.nan
            frames.append(df)
            time.sleep(1)
            
    return pd.concat(frames).sort_values(['orig', 'dest'])

In [ ]:
# Test some
origs = [
    [174.66339111328125, -36.45000844447082], 
    [174.76158142089844, -36.86533886128865],
    [174.85633850097656, -37.20517535620264],
]
dests = origs[:2]
matrix = get_matrix(origs, dests, mode='transit', departure_time='2017-06-01T08:00:00')
matrix_to_df(matrix, ['bingo', 'bongo', 'boom'], ['bingo', 'bongo'])

In [ ]:
# Estimate cost of job at 0.5/1000 USD/element beyond 2500 elements

def compute_google_cost(n, with_freebies=False):
    """
    If ``with_freebies``, then ignore the first 2500 elements, which are free.
    """
    d = OrderedDict()
    d['#rental areas'] = n
    N = 4*(n**2 - n)
    d['#elements needed for 4 modes'] = N
    d['exceeds 100000-element daily limit?'] = N > 100000 
    d['duration for job in minutes'] = (N/100)/60
    if with_freebies:
        d['cost for job in USD'] = (N - 2500)*(0.5/1000)
    else:
        d['cost for job in USD'] = N*(0.5/1000)
    
    return pd.Series(d)

compute_google_cost(f.shape[0])

In [ ]:
key = hp.get_secret('GOOGLE_API_KEY_PHIL')

for region in hp.REGIONS:   
    # Get points
    f = hp.get_data('rental_points', region)
    print('* ', region)
    print(compute_google_cost(f.shape[0]))
    
    # Build matrices
    departure_time='2017-06-01T07:30:00'
    for mode in af.MODES:
        %time m = build_matrix(f, mode=mode, departure_time=departure_time, key=key)
        n = m.shape[0]
        k = m[m['distance'].notnull()].shape[0]
        print(mode, 'hit rate', k/n)
        path = hp.get_path('commutes_' + mode, region)
        m.to_csv(path, index=False)

Compute Auckland and Wellington fares through their web APIs


In [2]:
def get_journey_auckland(orig, dest, departure_time=None, max_walk=1600):
    """
    INPUT
    ------
    orig : list
        WGS84 longitude-latitude pair
    dest : list
        WGS84 longitude-latitude pair
    departure_time : string
        ISO 8601 datetime; e.g. '2017-06-01T07:30:00'
    max_walk : float
        Maximum walking distance in meters for the journey
        
    OUTPUT
    ------
    dictionary
        Decoded JSON response of journey
    """
    url = 'https://api.at.govt.nz/v2/public-restricted/journeyplanner/silverRailIVU/plan'
    fromLoc ='{!s},{!s}'.format(orig[1], orig[0])
    toLoc ='{!s},{!s}'.format(dest[1], dest[0])
    if departure_time is None:
        departure_time = dt.datetime.now().strftime('%Y-%m-%dT%H:%M:%S')
    date = departure_time + '+12:00'  # Add UTC offset
    params = {
        'from': 'from',
        'to': 'to',
        'fromLoc': fromLoc,
        'toLoc': toLoc,
        'timeMode': 'A',
        'date': date, 
        'modes': 'BUS,TRAIN,FERRY',
        'operators': '',
        'optimize': 'QUICK',
        'maxWalk': max_walk,
        'maxChanges': '-1',
        'routes': '',
        'subscription-key': '323741614c1c4b9083299adefe100aa6',
    }
    r = requests.get(url, params=params)
    
    # Raise an error if bad request
    r.raise_for_status()

    return r.json()         

def get_fare_auckland(journey):
    """
    Given a journey of the form output by :func:`get_journey_auckland`, 
    return the journey's adult Hop card fare (float)'
    """
    if journey['error'] is None:
        f = journey['response']['itineraries'][0]['fareHopAdult']
        if f is None:
            fare = 0
        else:
            fare = f/100
    else:
        fare = None
    return fare

def get_journey_wellington(orig, dest, departure_time=None, max_walk=1600):
    """
    INPUT
    ------
    orig : list
        WGS84 longitude-latitude pair
    dest : list
        WGS84 longitude-latitude pair
    departure_time : string
        ISO 8601 datetime; e.g. '2017-06-01T07:30:00'
    max_walk : float
        Maximum walking distance in meters for the journey
        
    OUTPUT
    ------
    text
        HTML response of journey query
    """
    url = 'https://www.metlink.org.nz/journeyplanner/JourneyPlannerForm'
    from_coords ='{!s},{!s}'.format(orig[1], orig[0])
    to_coords ='{!s},{!s}'.format(dest[1], dest[0])
    
    if departure_time is None:
        departure_time = dt.datetime.now().strftime('%Y-%m-%dT%H:%M:%S')
    date, time = departure_time.split('T')
        
    params = {
        'From': 'from',
        'To': 'to', 
        'Via': '',
        'When': 'LeaveAfter',
        'Date': date,
        'Time': time,
        'MaxChanges': 5,
        'WalkingSpeed': 4,
        'MaxWalking': max_walk,
        'Modes[Train]': 'Train',
        'Modes[Bus]': 'Bus',
        'Modes[Ferry]': 'Ferry',
        'Modes[Cable+Car]': 'Cable Car',
        'ShowAdvanced': '',
        'FromCoords': from_coords,
        'ToCoords': to_coords,
        'ViaCoords': '',
        'action_doForm': 'Go',
    }
    r = requests.get(url, params=params)
    # Raise an error if bad request
    r.raise_for_status()
    return r.text         

# Estimate Wellington card fare discount
path = hp.DATA_DIR/'processed'/'wellington'/'transit_fares.csv'
f = pd.read_csv(path)
f['card/cash'] = f['card_fare']/f['cash_fare']
r = f['card/cash'].mean()
print('estimated Wellington card discount rate=', r)

def get_fare_wellington(journey, card_discount=r):
    """
    Given a journey of the form output by :func:`get_journey_wellington`, 
    extract the journey's adult cash fare (float), multiply it by the given
    discount rate to estimate the adult card fare, and return the result.
    """
    pattern = 'Total adult fare </span><strong>&#36;(\d+\.\d\d)</strong>'
    m = re.search(pattern, journey)
    if m:
        fare = float(m.group(1))
    else:
        fare = None
    return round(r*fare, 2)

def collect_fares(rental_points, departure_time, region):
    """
    """
    # Get all pairs of points excluding equal points
    f = rental_points[['rental_area', 'geometry']].copy()
    rows = [[o[0], o[1].coords[0], d[0], d[1].coords[0]] for o, d in product(f.values, f.values) if o[0] != d[0]]
    f = pd.DataFrame(rows, columns=['orig_name', 'orig', 'dest_name', 'dest'])

    if region == 'auckland':
        get_journey = get_journey_auckland
        get_fare = get_fare_auckland
        time_per_call = 3.6
    elif region == 'wellington':
        get_journey = get_journey_wellington
        get_fare = get_fare_wellington
        time_per_call = 2.4
    
    print('This will take about {:02f} minutes'.format(f.shape[0]*time_per_call/60))

    # Get journeys for each pair
    rows = []
    for __, row in f.iterrows():
        try:
            j = get_journey(row['orig'], row['dest'], departure_time=departure_time)
            fare = get_fare(j)
        except:
            fare = None
        rows.append([row['orig_name'], row['dest_name'], fare])

    g = pd.DataFrame(rows, columns=['orig_name', 'dest_name', 'card_fare'])
    return g


estimated Wellington card discount rate= 0.782401654073

In [5]:
# Test some
orig = [175.01092026711063, -36.93134386721964]  # Maraetai
orig2 = [174.76864676675842, -36.84997406733503]  # Central East
dest = [174.8151970336325, -36.89546015048722]  # Ellerslie
%time j = get_journey_auckland(orig2, dest, departure_time='2017-07-14T07:30:00')
j
#get_fare_auckland(j)

# orig = (174.7708511352539,-41.28394744513899)
# dest = (174.78861808776855,-41.297458248607995)
# %time r = get_journey_wellington(orig, dest, departure_time='2017-06-01T07:30:00')
# r
# get_fare_wellington(r)


CPU times: user 8 ms, sys: 0 ns, total: 8 ms
Wall time: 1.36 s
Out[5]:
{'error': None,
 'response': {'engine': 'silverRailIVU',
  'itineraries': [{'duration': 2460000,
    'durationStr': '41 minutes',
    'endTime': '2017-07-14T08:11',
    'endTimeStr': '8:11 am',
    'fareAdult': 550,
    'fareChild': 300,
    'fareError': None,
    'fareHopAdult': 315,
    'fareHopChild': 180,
    'fareHopTertiary': 235,
    'fareWarnings': {},
    'legs': [{'distance': '800 metres',
      'distanceExact': 800,
      'distanceStr': '800 metres',
      'duration': 720000,
      'durationStr': '12 min',
      'endLat': '-36.84429',
      'endLon': '174.76848',
      'endTime': '2017-07-14T07:42',
      'endTimeStr': '7:42 am',
      'fareAdult': 0,
      'fareChild': 0,
      'fareHopAdult': 0,
      'fareHopChild': 0,
      'fareHopTertiary': 0,
      'from': 'from',
      'isFastLeg': False,
      'isFirst': True,
      'isLast': False,
      'legGeometry': {'length': 47,
       'points': 'jg|_F_oui`@c@OEd@BP^bAiAQ{@P_@PQRKF]NY`@[PgAz@EN}@TOA{A[sBYQj@KEEACM}@WD]GAUB{DkAWSWUyBk@Fa@KE'},
      'messages': [],
      'mode': 'WALK',
      'startLat': '-36.8499741',
      'startLon': '174.7686468',
      'startTime': '2017-07-14T07:30',
      'startTimeStr': '7:30 am',
      'stops': [],
      'to': 'Britomart Train Station',
      'toStr': 'Walk to Britomart Train Station'},
     {'distance': 'undefined metres',
      'distanceStr': 'undefined metres',
      'duration': 780000,
      'durationStr': '13 min',
      'endLat': '-36.8984356',
      'endLon': '174.8082064',
      'endTime': '2017-07-14T07:55',
      'endTimeStr': '7:55 am',
      'fareAdult': 550,
      'fareChild': 300,
      'fareHopAdult': 0,
      'fareHopChild': 0,
      'fareHopTertiary': 0,
      'from': 'Britomart Train Station',
      'isFastLeg': True,
      'isFirst': False,
      'isLast': False,
      'legGeometry': {'length': 1140,
       'points': 'xc{_F_nui`@b@YDS`A_GXsAT_CZyA`Hua@l@{BNe@P_@b@i@TMZM`@GV?\\Dp@Xf@f@P^Px@Fn@L~BL`AX~@V`@RTZTn@Z`@Jf@Fl@?rASbFcA~DgAhBq@h]_PrJkEtQmIjAa@h@KXC|@?VB`ARt@\\v@l@bCnClFlEvAz@pAn@VAn@FtIzAdE`@nCCrIu@|HwBfEqBdEuClQqO|UyVtHiJbHcLnI_S~J}S`W{e@dHcKvA{AlDcChKyE'},
      'messages': [],
      'mode': 'TRAIN',
      'provider': 'AM',
      'routeCode': 'ONE',
      'routeName': 'Britomart Train Station to Onehunga Train Station',
      'startLat': '-36.84429',
      'startLon': '174.76848',
      'startTime': '2017-07-14T07:42',
      'startTimeStr': '7:42 am',
      'stops': [{'code': '133',
        'geometry': {'data': [-36.84429, 174.76848],
         'encoded': False,
         'type': 'point'}},
       {'code': '115',
        'geometry': {'data': [-36.86972, 174.77883],
         'encoded': False,
         'type': 'point'}},
       {'code': '112',
        'geometry': {'data': [-36.89847, 174.80811],
         'encoded': False,
         'type': 'point'}}],
      'to': 'Ellerslie Train Station',
      'toStr': 'Train to Ellerslie Train Station',
      'tripId': '51100070753-20170705140526_v55.10'},
     {'distance': '1088 metres',
      'distanceExact': 1088,
      'distanceStr': '1088 metres',
      'duration': 960000,
      'durationStr': '16 min',
      'endLat': '-36.8954602',
      'endLon': '174.815197',
      'endTime': '2017-07-14T08:11',
      'endTimeStr': '8:11 am',
      'fareAdult': 0,
      'fareChild': 0,
      'fareHopAdult': 0,
      'fareHopChild': 0,
      'fareHopTertiary': 0,
      'from': 'Ellerslie Train Station',
      'isFastLeg': False,
      'isFirst': False,
      'isLast': True,
      'legGeometry': {'length': 51,
       'points': 'nve`Fue}i`@GQoCfA]{AIECFBHDA@GCYJKHUbHcYsVuKZmA'},
      'messages': [],
      'mode': 'WALK',
      'startLat': '-36.89847',
      'startLon': '174.80811',
      'startTime': '2017-07-14T07:55',
      'startTimeStr': '7:55 am',
      'stops': [],
      'to': 'to',
      'toStr': 'Walk to to'}],
    'startTime': '2017-07-14T07:30',
    'startTimeStr': '7:30 am'},
   {'duration': 2520000,
    'durationStr': '42 minutes',
    'endTime': '2017-07-14T08:19',
    'endTimeStr': '8:19 am',
    'fareAdult': 550,
    'fareChild': 300,
    'fareError': None,
    'fareHopAdult': 315,
    'fareHopChild': 180,
    'fareHopTertiary': 235,
    'fareWarnings': {},
    'legs': [{'distance': '435 metres',
      'distanceExact': 435,
      'distanceStr': '435 metres',
      'duration': 420000,
      'durationStr': '7 min',
      'endLat': '-36.85056',
      'endLon': '174.77213',
      'endTime': '2017-07-14T07:44',
      'endTimeStr': '7:44 am',
      'fareAdult': 0,
      'fareChild': 0,
      'fareHopAdult': 0,
      'fareHopChild': 0,
      'fareHopTertiary': 0,
      'from': 'from',
      'isFastLeg': False,
      'isFirst': True,
      'isLast': False,
      'legGeometry': {'length': 33,
       'points': 'jg|_F_oui`@c@OTqAs@WV_BJUI[BSDBNCf@e@Ai@\\w@LK]q@HmA@q@CeBt@XDFHO'},
      'messages': [],
      'mode': 'WALK',
      'startLat': '-36.8499741',
      'startLon': '174.7686468',
      'startTime': '2017-07-14T07:37',
      'startTimeStr': '7:37 am',
      'stops': [],
      'to': 'Symonds St and Waterloo, 2 Symonds St',
      'toStr': 'Walk to Symonds St and Waterloo, 2 Symonds St'},
     {'distance': 'undefined metres',
      'distanceStr': 'undefined metres',
      'duration': 1620000,
      'durationStr': '27 min',
      'endLat': '-36.89895',
      'endLon': '174.8125',
      'endTime': '2017-07-14T08:11',
      'endTimeStr': '8:11 am',
      'fareAdult': 550,
      'fareChild': 300,
      'fareHopAdult': 0,
      'fareHopChild': 0,
      'fareHopTertiary': 0,
      'from': 'Symonds St and Waterloo, 2 Symonds St',
      'isFastLeg': False,
      'isFirst': False,
      'isLast': False,
      'legGeometry': {'length': 353,
       'points': '~j|_Fydvi`@IJDHbF|GtBzBbJbJhCxB^`@FKGJtErFzFlGhCfDHMILX\\PNx@b@La@rK}SJa@rBcONe@VW~@[BACKBJpHuBbCm@T?PBdKzBt@cGICHBdF{`@ACGAHBf@}D|@XTBf@CVBxQ~DBOCN~@NbGKBQ?P~JW?Q?P`BCd@w@`JeMGIFHnIoLEIDHn@}@TYtO{LNMCIBHbGwEdJoJGIFHpFyFCIBHjAqAh@q@tEcHz@sAAI@HpE{GfC_EGMFLlAkBlCyDvJaOKKJJhDcFDUHSr@s@j@_BNi@~EaSPu@ECDBtCiLCCBBT{@N{@@k@Gg@gB{HA]Fa@HKNAlEp@TGFGxCsLICFD|BkJGC'},
      'messages': [],
      'mode': 'BUS',
      'provider': 'HE',
      'routeCode': '500',
      'routeName': 'Britomart To Mission Heights Via Botany Town Centre',
      'startLat': '-36.85056',
      'startLon': '174.77213',
      'startTime': '2017-07-14T07:44',
      'startTimeStr': '7:44 am',
      'stops': [{'code': '7144',
        'geometry': {'data': [-36.85056, 174.77213],
         'encoded': False,
         'type': 'point'}},
       {'code': '7152',
        'geometry': {'data': [-36.85494, 174.76747],
         'encoded': False,
         'type': 'point'}},
       {'code': '7156',
        'geometry': {'data': [-36.85797, 174.76407],
         'encoded': False,
         'type': 'point'}},
       {'code': '1028',
        'geometry': {'data': [-36.86168, 174.77038],
         'encoded': False,
         'type': 'point'}},
       {'code': '7178',
        'geometry': {'data': [-36.86626, 174.77182],
         'encoded': False,
         'type': 'point'}},
       {'code': '7180',
        'geometry': {'data': [-36.86741, 174.77725],
         'encoded': False,
         'type': 'point'}},
       {'code': '7206',
        'geometry': {'data': [-36.87143, 174.77715],
         'encoded': False,
         'type': 'point'}},
       {'code': '7208',
        'geometry': {'data': [-36.87304, 174.77714],
         'encoded': False,
         'type': 'point'}},
       {'code': '7210',
        'geometry': {'data': [-36.87497, 174.77726],
         'encoded': False,
         'type': 'point'}},
       {'code': '7600',
        'geometry': {'data': [-36.87738, 174.77979],
         'encoded': False,
         'type': 'point'}},
       {'code': '7602',
        'geometry': {'data': [-36.87907, 174.78195],
         'encoded': False,
         'type': 'point'}},
       {'code': '7604',
        'geometry': {'data': [-36.88218, 174.78468],
         'encoded': False,
         'type': 'point'}},
       {'code': '7606',
        'geometry': {'data': [-36.88525, 174.7876],
         'encoded': False,
         'type': 'point'}},
       {'code': '7608',
        'geometry': {'data': [-36.88647, 174.78885],
         'encoded': False,
         'type': 'point'}},
       {'code': '7610',
        'geometry': {'data': [-36.88844, 174.79139],
         'encoded': False,
         'type': 'point'}},
       {'code': '7612',
        'geometry': {'data': [-36.89014, 174.79379],
         'encoded': False,
         'type': 'point'}},
       {'code': '7616',
        'geometry': {'data': [-36.89311, 174.79782],
         'encoded': False,
         'type': 'point'}},
       {'code': '7500',
        'geometry': {'data': [-36.89584, 174.80356],
         'encoded': False,
         'type': 'point'}},
       {'code': '7502',
        'geometry': {'data': [-36.89659, 174.80569],
         'encoded': False,
         'type': 'point'}},
       {'code': '7504',
        'geometry': {'data': [-36.89832, 174.81069],
         'encoded': False,
         'type': 'point'}},
       {'code': '7506',
        'geometry': {'data': [-36.89895, 174.8125],
         'encoded': False,
         'type': 'point'}}],
      'to': 'Main Hwy and Amy, 176 Main Hwy',
      'toStr': 'Bus to Main Hwy and Amy, 176 Main Hwy',
      'tripId': '5500072934-20170705140526_v55.10'},
     {'distance': '526 metres',
      'distanceExact': 526,
      'distanceStr': '526 metres',
      'duration': 480000,
      'durationStr': '8 min',
      'endLat': '-36.8954602',
      'endLon': '174.815197',
      'endTime': '2017-07-14T08:19',
      'endTimeStr': '8:19 am',
      'fareAdult': 0,
      'fareChild': 0,
      'fareHopAdult': 0,
      'fareHopChild': 0,
      'fareHopTertiary': 0,
      'from': 'Main Hwy and Amy, 176 Main Hwy',
      'isFastLeg': False,
      'isFirst': False,
      'isLast': True,
      'legGeometry': {'length': 13, 'points': 'lye`Fca~i`@HDT{@sVuKZmA'},
      'messages': [],
      'mode': 'WALK',
      'startLat': '-36.89895',
      'startLon': '174.8125',
      'startTime': '2017-07-14T08:11',
      'startTimeStr': '8:11 am',
      'stops': [],
      'to': 'to',
      'toStr': 'Walk to to'}],
    'startTime': '2017-07-14T07:37',
    'startTimeStr': '7:37 am'},
   {'duration': 2460000,
    'durationStr': '41 minutes',
    'endTime': '2017-07-14T08:25',
    'endTimeStr': '8:25 am',
    'fareAdult': 550,
    'fareChild': 300,
    'fareError': None,
    'fareHopAdult': 315,
    'fareHopChild': 180,
    'fareHopTertiary': 235,
    'fareWarnings': {},
    'legs': [{'distance': '435 metres',
      'distanceExact': 435,
      'distanceStr': '435 metres',
      'duration': 420000,
      'durationStr': '7 min',
      'endLat': '-36.85056',
      'endLon': '174.77213',
      'endTime': '2017-07-14T07:51',
      'endTimeStr': '7:51 am',
      'fareAdult': 0,
      'fareChild': 0,
      'fareHopAdult': 0,
      'fareHopChild': 0,
      'fareHopTertiary': 0,
      'from': 'from',
      'isFastLeg': False,
      'isFirst': True,
      'isLast': False,
      'legGeometry': {'length': 33,
       'points': 'jg|_F_oui`@c@OTqAs@WV_BJUI[BSDBNCf@e@Ai@\\w@LK]q@HmA@q@CeBt@XDFHO'},
      'messages': [],
      'mode': 'WALK',
      'startLat': '-36.8499741',
      'startLon': '174.7686468',
      'startTime': '2017-07-14T07:44',
      'startTimeStr': '7:44 am',
      'stops': [],
      'to': 'Symonds St and Waterloo, 2 Symonds St',
      'toStr': 'Walk to Symonds St and Waterloo, 2 Symonds St'},
     {'distance': 'undefined metres',
      'distanceStr': 'undefined metres',
      'duration': 1560000,
      'durationStr': '26 min',
      'endLat': '-36.89895',
      'endLon': '174.8125',
      'endTime': '2017-07-14T08:17',
      'endTimeStr': '8:17 am',
      'fareAdult': 550,
      'fareChild': 300,
      'fareHopAdult': 0,
      'fareHopChild': 0,
      'fareHopTertiary': 0,
      'from': 'Symonds St and Waterloo, 2 Symonds St',
      'isFastLeg': False,
      'isFirst': False,
      'isLast': False,
      'legGeometry': {'length': 349,
       'points': '~j|_Fydvi`@IJDHbF|GtBzBbJbJhCxB^`@FKGJtErFzFlGhCfDHMILX\\PNx@b@La@rK}SJa@rBcONe@VW~@[BACKBJpHuBbCm@T?PBdKzBt@cGICHBdF{`@ACGAHBf@}D|@XTBf@CVBxQ~DBOCN~@NfSc@?Q?P`BCd@w@`JeMGIFHnIoLEIDHn@}@TYtO{LNMCIBHbGwEdJoJGIFHpFyFCIBHjAqAh@q@tEcHz@sAAI@HpE{GfC_EGMFLlAkBlCyDvJaOKKJJhDcFDUHSr@s@j@_BNi@~EaSPu@ECDBtCiLCCBBT{@N{@@k@Gg@gB{HA]Fa@HKNAlEp@TGFGxCsLICFD|BkJGC'},
      'messages': [],
      'mode': 'BUS',
      'provider': 'HE',
      'routeCode': '550',
      'routeName': 'Britomart To Cockle Bay Via Newmarket',
      'startLat': '-36.85056',
      'startLon': '174.77213',
      'startTime': '2017-07-14T07:51',
      'startTimeStr': '7:51 am',
      'stops': [{'code': '7144',
        'geometry': {'data': [-36.85056, 174.77213],
         'encoded': False,
         'type': 'point'}},
       {'code': '7152',
        'geometry': {'data': [-36.85494, 174.76747],
         'encoded': False,
         'type': 'point'}},
       {'code': '7156',
        'geometry': {'data': [-36.85797, 174.76407],
         'encoded': False,
         'type': 'point'}},
       {'code': '1028',
        'geometry': {'data': [-36.86168, 174.77038],
         'encoded': False,
         'type': 'point'}},
       {'code': '7178',
        'geometry': {'data': [-36.86626, 174.77182],
         'encoded': False,
         'type': 'point'}},
       {'code': '7180',
        'geometry': {'data': [-36.86741, 174.77725],
         'encoded': False,
         'type': 'point'}},
       {'code': '7206',
        'geometry': {'data': [-36.87143, 174.77715],
         'encoded': False,
         'type': 'point'}},
       {'code': '7210',
        'geometry': {'data': [-36.87497, 174.77726],
         'encoded': False,
         'type': 'point'}},
       {'code': '7600',
        'geometry': {'data': [-36.87738, 174.77979],
         'encoded': False,
         'type': 'point'}},
       {'code': '7602',
        'geometry': {'data': [-36.87907, 174.78195],
         'encoded': False,
         'type': 'point'}},
       {'code': '7604',
        'geometry': {'data': [-36.88218, 174.78468],
         'encoded': False,
         'type': 'point'}},
       {'code': '7606',
        'geometry': {'data': [-36.88525, 174.7876],
         'encoded': False,
         'type': 'point'}},
       {'code': '7608',
        'geometry': {'data': [-36.88647, 174.78885],
         'encoded': False,
         'type': 'point'}},
       {'code': '7610',
        'geometry': {'data': [-36.88844, 174.79139],
         'encoded': False,
         'type': 'point'}},
       {'code': '7612',
        'geometry': {'data': [-36.89014, 174.79379],
         'encoded': False,
         'type': 'point'}},
       {'code': '7616',
        'geometry': {'data': [-36.89311, 174.79782],
         'encoded': False,
         'type': 'point'}},
       {'code': '7500',
        'geometry': {'data': [-36.89584, 174.80356],
         'encoded': False,
         'type': 'point'}},
       {'code': '7502',
        'geometry': {'data': [-36.89659, 174.80569],
         'encoded': False,
         'type': 'point'}},
       {'code': '7504',
        'geometry': {'data': [-36.89832, 174.81069],
         'encoded': False,
         'type': 'point'}},
       {'code': '7506',
        'geometry': {'data': [-36.89895, 174.8125],
         'encoded': False,
         'type': 'point'}}],
      'to': 'Main Hwy and Amy, 176 Main Hwy',
      'toStr': 'Bus to Main Hwy and Amy, 176 Main Hwy',
      'tripId': '5550011199-20170705140526_v55.10'},
     {'distance': '526 metres',
      'distanceExact': 526,
      'distanceStr': '526 metres',
      'duration': 480000,
      'durationStr': '8 min',
      'endLat': '-36.8954602',
      'endLon': '174.815197',
      'endTime': '2017-07-14T08:25',
      'endTimeStr': '8:25 am',
      'fareAdult': 0,
      'fareChild': 0,
      'fareHopAdult': 0,
      'fareHopChild': 0,
      'fareHopTertiary': 0,
      'from': 'Main Hwy and Amy, 176 Main Hwy',
      'isFastLeg': False,
      'isFirst': False,
      'isLast': True,
      'legGeometry': {'length': 13, 'points': 'lye`Fca~i`@HDT{@sVuKZmA'},
      'messages': [],
      'mode': 'WALK',
      'startLat': '-36.89895',
      'startLon': '174.8125',
      'startTime': '2017-07-14T08:17',
      'startTimeStr': '8:17 am',
      'stops': [],
      'to': 'to',
      'toStr': 'Walk to to'}],
    'startTime': '2017-07-14T07:44',
    'startTimeStr': '7:44 am'},
   {'duration': 2460000,
    'durationStr': '41 minutes',
    'endTime': '2017-07-14T08:33',
    'endTimeStr': '8:33 am',
    'fareAdult': 550,
    'fareChild': 300,
    'fareError': None,
    'fareHopAdult': 315,
    'fareHopChild': 180,
    'fareHopTertiary': 235,
    'fareWarnings': {},
    'legs': [{'distance': '435 metres',
      'distanceExact': 435,
      'distanceStr': '435 metres',
      'duration': 420000,
      'durationStr': '7 min',
      'endLat': '-36.85056',
      'endLon': '174.77213',
      'endTime': '2017-07-14T07:59',
      'endTimeStr': '7:59 am',
      'fareAdult': 0,
      'fareChild': 0,
      'fareHopAdult': 0,
      'fareHopChild': 0,
      'fareHopTertiary': 0,
      'from': 'from',
      'isFastLeg': False,
      'isFirst': True,
      'isLast': False,
      'legGeometry': {'length': 33,
       'points': 'jg|_F_oui`@c@OTqAs@WV_BJUI[BSDBNCf@e@Ai@\\w@LK]q@HmA@q@CeBt@XDFHO'},
      'messages': [],
      'mode': 'WALK',
      'startLat': '-36.8499741',
      'startLon': '174.7686468',
      'startTime': '2017-07-14T07:52',
      'startTimeStr': '7:52 am',
      'stops': [],
      'to': 'Symonds St and Waterloo, 2 Symonds St',
      'toStr': 'Walk to Symonds St and Waterloo, 2 Symonds St'},
     {'distance': 'undefined metres',
      'distanceStr': 'undefined metres',
      'duration': 1560000,
      'durationStr': '26 min',
      'endLat': '-36.89895',
      'endLon': '174.8125',
      'endTime': '2017-07-14T08:25',
      'endTimeStr': '8:25 am',
      'fareAdult': 550,
      'fareChild': 300,
      'fareHopAdult': 0,
      'fareHopChild': 0,
      'fareHopTertiary': 0,
      'from': 'Symonds St and Waterloo, 2 Symonds St',
      'isFastLeg': False,
      'isFirst': False,
      'isLast': False,
      'legGeometry': {'length': 353,
       'points': '~j|_Fydvi`@IJDHbF|GtBzBbJbJhCxB^`@FKGJtErFzFlGhCfDHMILX\\PNx@b@La@rK}SJa@rBcONe@VW~@[BACKBJpHuBbCm@T?PBdKzBt@cGICHBdF{`@ACGAHBf@}D|@XTBf@CVBxQ~DBOCN~@NbGKBQ?P~JW?Q?P`BCd@w@`JeMGIFHnIoLEIDHn@}@TYtO{LNMCIBHbGwEdJoJGIFHpFyFCIBHjAqAh@q@tEcHz@sAAI@HpE{GfC_EGMFLlAkBlCyDvJaOKKJJhDcFDUHSr@s@j@_BNi@~EaSPu@ECDBtCiLCCBBT{@N{@@k@Gg@gB{HA]Fa@HKNAlEp@TGFGxCsLICFD|BkJGC'},
      'messages': [],
      'mode': 'BUS',
      'provider': 'HE',
      'routeCode': '500',
      'routeName': 'Britomart To Botany Town Centre Via Ti Rakau Dr',
      'startLat': '-36.85056',
      'startLon': '174.77213',
      'startTime': '2017-07-14T07:59',
      'startTimeStr': '7:59 am',
      'stops': [{'code': '7144',
        'geometry': {'data': [-36.85056, 174.77213],
         'encoded': False,
         'type': 'point'}},
       {'code': '7152',
        'geometry': {'data': [-36.85494, 174.76747],
         'encoded': False,
         'type': 'point'}},
       {'code': '7156',
        'geometry': {'data': [-36.85797, 174.76407],
         'encoded': False,
         'type': 'point'}},
       {'code': '1028',
        'geometry': {'data': [-36.86168, 174.77038],
         'encoded': False,
         'type': 'point'}},
       {'code': '7178',
        'geometry': {'data': [-36.86626, 174.77182],
         'encoded': False,
         'type': 'point'}},
       {'code': '7180',
        'geometry': {'data': [-36.86741, 174.77725],
         'encoded': False,
         'type': 'point'}},
       {'code': '7206',
        'geometry': {'data': [-36.87143, 174.77715],
         'encoded': False,
         'type': 'point'}},
       {'code': '7208',
        'geometry': {'data': [-36.87304, 174.77714],
         'encoded': False,
         'type': 'point'}},
       {'code': '7210',
        'geometry': {'data': [-36.87497, 174.77726],
         'encoded': False,
         'type': 'point'}},
       {'code': '7600',
        'geometry': {'data': [-36.87738, 174.77979],
         'encoded': False,
         'type': 'point'}},
       {'code': '7602',
        'geometry': {'data': [-36.87907, 174.78195],
         'encoded': False,
         'type': 'point'}},
       {'code': '7604',
        'geometry': {'data': [-36.88218, 174.78468],
         'encoded': False,
         'type': 'point'}},
       {'code': '7606',
        'geometry': {'data': [-36.88525, 174.7876],
         'encoded': False,
         'type': 'point'}},
       {'code': '7608',
        'geometry': {'data': [-36.88647, 174.78885],
         'encoded': False,
         'type': 'point'}},
       {'code': '7610',
        'geometry': {'data': [-36.88844, 174.79139],
         'encoded': False,
         'type': 'point'}},
       {'code': '7612',
        'geometry': {'data': [-36.89014, 174.79379],
         'encoded': False,
         'type': 'point'}},
       {'code': '7616',
        'geometry': {'data': [-36.89311, 174.79782],
         'encoded': False,
         'type': 'point'}},
       {'code': '7500',
        'geometry': {'data': [-36.89584, 174.80356],
         'encoded': False,
         'type': 'point'}},
       {'code': '7502',
        'geometry': {'data': [-36.89659, 174.80569],
         'encoded': False,
         'type': 'point'}},
       {'code': '7504',
        'geometry': {'data': [-36.89832, 174.81069],
         'encoded': False,
         'type': 'point'}},
       {'code': '7506',
        'geometry': {'data': [-36.89895, 174.8125],
         'encoded': False,
         'type': 'point'}}],
      'to': 'Main Hwy and Amy, 176 Main Hwy',
      'toStr': 'Bus to Main Hwy and Amy, 176 Main Hwy',
      'tripId': '5500073032-20170705140526_v55.10'},
     {'distance': '526 metres',
      'distanceExact': 526,
      'distanceStr': '526 metres',
      'duration': 480000,
      'durationStr': '8 min',
      'endLat': '-36.8954602',
      'endLon': '174.815197',
      'endTime': '2017-07-14T08:33',
      'endTimeStr': '8:33 am',
      'fareAdult': 0,
      'fareChild': 0,
      'fareHopAdult': 0,
      'fareHopChild': 0,
      'fareHopTertiary': 0,
      'from': 'Main Hwy and Amy, 176 Main Hwy',
      'isFastLeg': False,
      'isFirst': False,
      'isLast': True,
      'legGeometry': {'length': 13, 'points': 'lye`Fca~i`@HDT{@sVuKZmA'},
      'messages': [],
      'mode': 'WALK',
      'startLat': '-36.89895',
      'startLon': '174.8125',
      'startTime': '2017-07-14T08:25',
      'startTimeStr': '8:25 am',
      'stops': [],
      'to': 'to',
      'toStr': 'Walk to to'}],
    'startTime': '2017-07-14T07:52',
    'startTimeStr': '7:52 am'},
   {'duration': 2100000,
    'durationStr': '35 minutes',
    'endTime': '2017-07-14T08:39',
    'endTimeStr': '8:39 am',
    'fareAdult': 550,
    'fareChild': 300,
    'fareError': None,
    'fareHopAdult': 315,
    'fareHopChild': 180,
    'fareHopTertiary': 235,
    'fareWarnings': {},
    'legs': [{'distance': '435 metres',
      'distanceExact': 435,
      'distanceStr': '435 metres',
      'duration': 420000,
      'durationStr': '7 min',
      'endLat': '-36.85056',
      'endLon': '174.77213',
      'endTime': '2017-07-14T08:11',
      'endTimeStr': '8:11 am',
      'fareAdult': 0,
      'fareChild': 0,
      'fareHopAdult': 0,
      'fareHopChild': 0,
      'fareHopTertiary': 0,
      'from': 'from',
      'isFastLeg': False,
      'isFirst': True,
      'isLast': False,
      'legGeometry': {'length': 33,
       'points': 'jg|_F_oui`@c@OTqAs@WV_BJUI[BSDBNCf@e@Ai@\\w@LK]q@HmA@q@CeBt@XDFHO'},
      'messages': [],
      'mode': 'WALK',
      'startLat': '-36.8499741',
      'startLon': '174.7686468',
      'startTime': '2017-07-14T08:04',
      'startTimeStr': '8:04 am',
      'stops': [],
      'to': 'Symonds St and Waterloo, 2 Symonds St',
      'toStr': 'Walk to Symonds St and Waterloo, 2 Symonds St'},
     {'distance': 'undefined metres',
      'distanceStr': 'undefined metres',
      'duration': 1560000,
      'durationStr': '26 min',
      'endLat': '-36.89615',
      'endLon': '174.81431',
      'endTime': '2017-07-14T08:37',
      'endTimeStr': '8:37 am',
      'fareAdult': 550,
      'fareChild': 300,
      'fareHopAdult': 0,
      'fareHopChild': 0,
      'fareHopTertiary': 0,
      'from': 'Symonds St and Waterloo, 2 Symonds St',
      'isFastLeg': False,
      'isFirst': False,
      'isLast': False,
      'legGeometry': {'length': 356,
       'points': '~j|_Fydvi`@IJhFfHzB`CbK`KlB~ADKEJFH|G`I`ElEhCfDHMILX\\PNx@b@La@rK}SJa@rBcONe@VW~@[BACKBJlA_@jCq@VK~@WbCm@T?PBdKzBb@_D?MICHBvFsc@ICHBf@}D|@XTBf@CVBdPnDBOCNjAVf@FbGKBQ?P~JW?Q?P`BCd@w@`JeMGIFHnIoLEIDHn@}@TYtO{LNMCIBHbGwEdJoJGIFHpFyFCIBHjAqAh@q@tEcHz@sAAI@HpE{GfC_EGMFLlAkBlCyDvJaOKKJJhDcFDUHSr@s@j@_BNi@~EaSPu@ECDBtCiLCCBBT{@N{@@k@Gg@gB{HA]Fa@HKNAlEp@TGFGxCsLICFD|BkJGCHBR{@qBw@wMaGCF'},
      'messages': [],
      'mode': 'BUS',
      'provider': 'NZBML',
      'routeCode': '595',
      'routeName': 'Britomart To Glen Innes Via Panmure',
      'startLat': '-36.85056',
      'startLon': '174.77213',
      'startTime': '2017-07-14T08:11',
      'startTimeStr': '8:11 am',
      'stops': [{'code': '7144',
        'geometry': {'data': [-36.85056, 174.77213],
         'encoded': False,
         'type': 'point'}},
       {'code': '7150',
        'geometry': {'data': [-36.85482, 174.76759],
         'encoded': False,
         'type': 'point'}},
       {'code': '7156',
        'geometry': {'data': [-36.85797, 174.76407],
         'encoded': False,
         'type': 'point'}},
       {'code': '1028',
        'geometry': {'data': [-36.86168, 174.77038],
         'encoded': False,
         'type': 'point'}},
       {'code': '7176',
        'geometry': {'data': [-36.86617, 174.77139],
         'encoded': False,
         'type': 'point'}},
       {'code': '7180',
        'geometry': {'data': [-36.86741, 174.77725],
         'encoded': False,
         'type': 'point'}},
       {'code': '7204',
        'geometry': {'data': [-36.87116, 174.77723],
         'encoded': False,
         'type': 'point'}},
       {'code': '7208',
        'geometry': {'data': [-36.87304, 174.77714],
         'encoded': False,
         'type': 'point'}},
       {'code': '7210',
        'geometry': {'data': [-36.87497, 174.77726],
         'encoded': False,
         'type': 'point'}},
       {'code': '7600',
        'geometry': {'data': [-36.87738, 174.77979],
         'encoded': False,
         'type': 'point'}},
       {'code': '7602',
        'geometry': {'data': [-36.87907, 174.78195],
         'encoded': False,
         'type': 'point'}},
       {'code': '7604',
        'geometry': {'data': [-36.88218, 174.78468],
         'encoded': False,
         'type': 'point'}},
       {'code': '7606',
        'geometry': {'data': [-36.88525, 174.7876],
         'encoded': False,
         'type': 'point'}},
       {'code': '7608',
        'geometry': {'data': [-36.88647, 174.78885],
         'encoded': False,
         'type': 'point'}},
       {'code': '7610',
        'geometry': {'data': [-36.88844, 174.79139],
         'encoded': False,
         'type': 'point'}},
       {'code': '7612',
        'geometry': {'data': [-36.89014, 174.79379],
         'encoded': False,
         'type': 'point'}},
       {'code': '7616',
        'geometry': {'data': [-36.89311, 174.79782],
         'encoded': False,
         'type': 'point'}},
       {'code': '7500',
        'geometry': {'data': [-36.89584, 174.80356],
         'encoded': False,
         'type': 'point'}},
       {'code': '7502',
        'geometry': {'data': [-36.89659, 174.80569],
         'encoded': False,
         'type': 'point'}},
       {'code': '7504',
        'geometry': {'data': [-36.89832, 174.81069],
         'encoded': False,
         'type': 'point'}},
       {'code': '7506',
        'geometry': {'data': [-36.89895, 174.8125],
         'encoded': False,
         'type': 'point'}},
       {'code': '7456',
        'geometry': {'data': [-36.89615, 174.81431],
         'encoded': False,
         'type': 'point'}}],
      'to': 'Amy St and Ballin, 29 Amy St',
      'toStr': 'Bus to Amy St and Ballin, 29 Amy St',
      'tripId': '14595039980-20170705140526_v55.10'},
     {'distance': '142 metres',
      'distanceExact': 142,
      'distanceStr': '142 metres',
      'duration': 120000,
      'durationStr': '1 min',
      'endLat': '-36.8954602',
      'endLon': '174.815197',
      'endTime': '2017-07-14T08:39',
      'endTimeStr': '8:39 am',
      'fareAdult': 0,
      'fareChild': 0,
      'fareHopAdult': 0,
      'fareHopChild': 0,
      'fareHopTertiary': 0,
      'from': 'Amy St and Ballin, 29 Amy St',
      'isFastLeg': False,
      'isFirst': False,
      'isLast': True,
      'legGeometry': {'length': 5, 'points': '|ge`Fml~i`@BEgD{AZmA'},
      'messages': [],
      'mode': 'WALK',
      'startLat': '-36.89615',
      'startLon': '174.81431',
      'startTime': '2017-07-14T08:37',
      'startTimeStr': '8:37 am',
      'stops': [],
      'to': 'to',
      'toStr': 'Walk to to'}],
    'startTime': '2017-07-14T08:04',
    'startTimeStr': '8:04 am'},
   {'duration': 2520000,
    'durationStr': '42 minutes',
    'endTime': '2017-07-14T08:49',
    'endTimeStr': '8:49 am',
    'fareAdult': 550,
    'fareChild': 300,
    'fareError': None,
    'fareHopAdult': 315,
    'fareHopChild': 180,
    'fareHopTertiary': 235,
    'fareWarnings': {},
    'legs': [{'distance': '435 metres',
      'distanceExact': 435,
      'distanceStr': '435 metres',
      'duration': 420000,
      'durationStr': '7 min',
      'endLat': '-36.85056',
      'endLon': '174.77213',
      'endTime': '2017-07-14T08:14',
      'endTimeStr': '8:14 am',
      'fareAdult': 0,
      'fareChild': 0,
      'fareHopAdult': 0,
      'fareHopChild': 0,
      'fareHopTertiary': 0,
      'from': 'from',
      'isFastLeg': False,
      'isFirst': True,
      'isLast': False,
      'legGeometry': {'length': 33,
       'points': 'jg|_F_oui`@c@OTqAs@WV_BJUI[BSDBNCf@e@Ai@\\w@LK]q@HmA@q@CeBt@XDFHO'},
      'messages': [],
      'mode': 'WALK',
      'startLat': '-36.8499741',
      'startLon': '174.7686468',
      'startTime': '2017-07-14T08:07',
      'startTimeStr': '8:07 am',
      'stops': [],
      'to': 'Symonds St and Waterloo, 2 Symonds St',
      'toStr': 'Walk to Symonds St and Waterloo, 2 Symonds St'},
     {'distance': 'undefined metres',
      'distanceStr': 'undefined metres',
      'duration': 1620000,
      'durationStr': '27 min',
      'endLat': '-36.89895',
      'endLon': '174.8125',
      'endTime': '2017-07-14T08:41',
      'endTimeStr': '8:41 am',
      'fareAdult': 550,
      'fareChild': 300,
      'fareHopAdult': 0,
      'fareHopChild': 0,
      'fareHopTertiary': 0,
      'from': 'Symonds St and Waterloo, 2 Symonds St',
      'isFastLeg': False,
      'isFirst': False,
      'isLast': False,
      'legGeometry': {'length': 353,
       'points': '~j|_Fydvi`@IJDHbF|GtBzBbJbJhCxB^`@FKGJtErFzFlGhCfDHMILX\\PNx@b@La@rK}SJa@rBcONe@VW~@[BACKBJpHuBbCm@T?PBdKzBt@cGICHBdF{`@ACGAHBf@}D|@XTBf@CVBxQ~DBOCN~@NbGKBQ?P~JW?Q?P`BCd@w@`JeMGIFHnIoLEIDHn@}@TYtO{LNMCIBHbGwEdJoJGIFHpFyFCIBHjAqAh@q@tEcHz@sAAI@HpE{GfC_EGMFLlAkBlCyDvJaOKKJJhDcFDUHSr@s@j@_BNi@~EaSPu@ECDBtCiLCCBBT{@N{@@k@Gg@gB{HA]Fa@HKNAlEp@TGFGxCsLICFD|BkJGC'},
      'messages': [],
      'mode': 'BUS',
      'provider': 'HE',
      'routeCode': '500',
      'routeName': 'Britomart To Mission Heights Via Botany Town Centre',
      'startLat': '-36.85056',
      'startLon': '174.77213',
      'startTime': '2017-07-14T08:14',
      'startTimeStr': '8:14 am',
      'stops': [{'code': '7144',
        'geometry': {'data': [-36.85056, 174.77213],
         'encoded': False,
         'type': 'point'}},
       {'code': '7152',
        'geometry': {'data': [-36.85494, 174.76747],
         'encoded': False,
         'type': 'point'}},
       {'code': '7156',
        'geometry': {'data': [-36.85797, 174.76407],
         'encoded': False,
         'type': 'point'}},
       {'code': '1028',
        'geometry': {'data': [-36.86168, 174.77038],
         'encoded': False,
         'type': 'point'}},
       {'code': '7178',
        'geometry': {'data': [-36.86626, 174.77182],
         'encoded': False,
         'type': 'point'}},
       {'code': '7180',
        'geometry': {'data': [-36.86741, 174.77725],
         'encoded': False,
         'type': 'point'}},
       {'code': '7206',
        'geometry': {'data': [-36.87143, 174.77715],
         'encoded': False,
         'type': 'point'}},
       {'code': '7208',
        'geometry': {'data': [-36.87304, 174.77714],
         'encoded': False,
         'type': 'point'}},
       {'code': '7210',
        'geometry': {'data': [-36.87497, 174.77726],
         'encoded': False,
         'type': 'point'}},
       {'code': '7600',
        'geometry': {'data': [-36.87738, 174.77979],
         'encoded': False,
         'type': 'point'}},
       {'code': '7602',
        'geometry': {'data': [-36.87907, 174.78195],
         'encoded': False,
         'type': 'point'}},
       {'code': '7604',
        'geometry': {'data': [-36.88218, 174.78468],
         'encoded': False,
         'type': 'point'}},
       {'code': '7606',
        'geometry': {'data': [-36.88525, 174.7876],
         'encoded': False,
         'type': 'point'}},
       {'code': '7608',
        'geometry': {'data': [-36.88647, 174.78885],
         'encoded': False,
         'type': 'point'}},
       {'code': '7610',
        'geometry': {'data': [-36.88844, 174.79139],
         'encoded': False,
         'type': 'point'}},
       {'code': '7612',
        'geometry': {'data': [-36.89014, 174.79379],
         'encoded': False,
         'type': 'point'}},
       {'code': '7616',
        'geometry': {'data': [-36.89311, 174.79782],
         'encoded': False,
         'type': 'point'}},
       {'code': '7500',
        'geometry': {'data': [-36.89584, 174.80356],
         'encoded': False,
         'type': 'point'}},
       {'code': '7502',
        'geometry': {'data': [-36.89659, 174.80569],
         'encoded': False,
         'type': 'point'}},
       {'code': '7504',
        'geometry': {'data': [-36.89832, 174.81069],
         'encoded': False,
         'type': 'point'}},
       {'code': '7506',
        'geometry': {'data': [-36.89895, 174.8125],
         'encoded': False,
         'type': 'point'}}],
      'to': 'Main Hwy and Amy, 176 Main Hwy',
      'toStr': 'Bus to Main Hwy and Amy, 176 Main Hwy',
      'tripId': '5500072933-20170705140526_v55.10'},
     {'distance': '526 metres',
      'distanceExact': 526,
      'distanceStr': '526 metres',
      'duration': 480000,
      'durationStr': '8 min',
      'endLat': '-36.8954602',
      'endLon': '174.815197',
      'endTime': '2017-07-14T08:49',
      'endTimeStr': '8:49 am',
      'fareAdult': 0,
      'fareChild': 0,
      'fareHopAdult': 0,
      'fareHopChild': 0,
      'fareHopTertiary': 0,
      'from': 'Main Hwy and Amy, 176 Main Hwy',
      'isFastLeg': False,
      'isFirst': False,
      'isLast': True,
      'legGeometry': {'length': 13, 'points': 'lye`Fca~i`@HDT{@sVuKZmA'},
      'messages': [],
      'mode': 'WALK',
      'startLat': '-36.89895',
      'startLon': '174.8125',
      'startTime': '2017-07-14T08:41',
      'startTimeStr': '8:41 am',
      'stops': [],
      'to': 'to',
      'toStr': 'Walk to to'}],
    'startTime': '2017-07-14T08:07',
    'startTimeStr': '8:07 am'},
   {'duration': 2700000,
    'durationStr': 'an hour',
    'endTime': '2017-07-14T08:59',
    'endTimeStr': '8:59 am',
    'fareAdult': 550,
    'fareChild': 300,
    'fareError': None,
    'fareHopAdult': 315,
    'fareHopChild': 180,
    'fareHopTertiary': 235,
    'fareWarnings': {},
    'legs': [{'distance': '800 metres',
      'distanceExact': 800,
      'distanceStr': '800 metres',
      'duration': 720000,
      'durationStr': '12 min',
      'endLat': '-36.84429',
      'endLon': '174.76848',
      'endTime': '2017-07-14T08:26',
      'endTimeStr': '8:26 am',
      'fareAdult': 0,
      'fareChild': 0,
      'fareHopAdult': 0,
      'fareHopChild': 0,
      'fareHopTertiary': 0,
      'from': 'from',
      'isFastLeg': False,
      'isFirst': True,
      'isLast': False,
      'legGeometry': {'length': 47,
       'points': 'jg|_F_oui`@c@OEd@BP^bAiAQ{@P_@PQRKF]NY`@[PgAz@EN}@TOA{A[sBYQj@KEEACM}@WD]GAUB{DkAWSWUyBk@Fa@KE'},
      'messages': [],
      'mode': 'WALK',
      'startLat': '-36.8499741',
      'startLon': '174.7686468',
      'startTime': '2017-07-14T08:14',
      'startTimeStr': '8:14 am',
      'stops': [],
      'to': 'Britomart Train Station',
      'toStr': 'Walk to Britomart Train Station'},
     {'distance': 'undefined metres',
      'distanceStr': 'undefined metres',
      'duration': 1020000,
      'durationStr': '17 min',
      'endLat': '-36.8984373',
      'endLon': '174.8082082',
      'endTime': '2017-07-14T08:43',
      'endTimeStr': '8:43 am',
      'fareAdult': 550,
      'fareChild': 300,
      'fareHopAdult': 0,
      'fareHopChild': 0,
      'fareHopTertiary': 0,
      'from': 'Britomart Train Station',
      'isFastLeg': True,
      'isFirst': False,
      'isLast': False,
      'legGeometry': {'length': 700,
       'points': 'xc{_F_nui`@|N}w@`@mAl@u@b@Sf@Ih@?n@Jf@b@\\l@ZdAZhEJf@Vp@h@|@fBz@b@HjAAzGwAjGgBhJaEht@o\\|B?n@DvA`@fHdH`DxBnBjAlMzBrC\\b@BjA?rAM|CQpBUlB]rCq@`Bk@hBy@jC{A|@m@|AmAnQ{O|HaIh@g@@@`JiJtGwH\\k@l@w@zBuDdEuHxAwDtD{IbG{LxAaDAArF{KpD}FxIwQtCqEhFqGzDsDdLyE'},
      'messages': [],
      'mode': 'TRAIN',
      'provider': 'AM',
      'routeCode': 'STH',
      'routeName': 'Britomart Train Station to Papakura Train Station',
      'startLat': '-36.84429',
      'startLon': '174.76848',
      'startTime': '2017-07-14T08:26',
      'startTimeStr': '8:26 am',
      'stops': [{'code': '133',
        'geometry': {'data': [-36.84429, 174.76848],
         'encoded': False,
         'type': 'point'}},
       {'code': '140',
        'geometry': {'data': [-36.85473, 174.7774],
         'encoded': False,
         'type': 'point'}},
       {'code': '115',
        'geometry': {'data': [-36.86972, 174.77883],
         'encoded': False,
         'type': 'point'}},
       {'code': '114',
        'geometry': {'data': [-36.88138, 174.78542],
         'encoded': False,
         'type': 'point'}},
       {'code': '113',
        'geometry': {'data': [-36.88966, 174.79742],
         'encoded': False,
         'type': 'point'}},
       {'code': '112',
        'geometry': {'data': [-36.89847, 174.80811],
         'encoded': False,
         'type': 'point'}}],
      'to': 'Ellerslie Train Station',
      'toStr': 'Train to Ellerslie Train Station',
      'tripId': '23100070962-20170705140526_v55.10'},
     {'distance': '1088 metres',
      'distanceExact': 1088,
      'distanceStr': '1088 metres',
      'duration': 960000,
      'durationStr': '16 min',
      'endLat': '-36.8954602',
      'endLon': '174.815197',
      'endTime': '2017-07-14T08:59',
      'endTimeStr': '8:59 am',
      'fareAdult': 0,
      'fareChild': 0,
      'fareHopAdult': 0,
      'fareHopChild': 0,
      'fareHopTertiary': 0,
      'from': 'Ellerslie Train Station',
      'isFastLeg': False,
      'isFirst': False,
      'isLast': True,
      'legGeometry': {'length': 51,
       'points': 'nve`Fue}i`@GQoCfA]{AIECFBHDA@GCYJKHUbHcYsVuKZmA'},
      'messages': [],
      'mode': 'WALK',
      'startLat': '-36.89847',
      'startLon': '174.80811',
      'startTime': '2017-07-14T08:43',
      'startTimeStr': '8:43 am',
      'stops': [],
      'to': 'to',
      'toStr': 'Walk to to'}],
    'startTime': '2017-07-14T08:14',
    'startTimeStr': '8:14 am'},
   {'duration': 2460000,
    'durationStr': '41 minutes',
    'endTime': '2017-07-14T09:03',
    'endTimeStr': '9:03 am',
    'fareAdult': 550,
    'fareChild': 300,
    'fareError': None,
    'fareHopAdult': 315,
    'fareHopChild': 180,
    'fareHopTertiary': 235,
    'fareWarnings': {},
    'legs': [{'distance': '435 metres',
      'distanceExact': 435,
      'distanceStr': '435 metres',
      'duration': 420000,
      'durationStr': '7 min',
      'endLat': '-36.85056',
      'endLon': '174.77213',
      'endTime': '2017-07-14T08:29',
      'endTimeStr': '8:29 am',
      'fareAdult': 0,
      'fareChild': 0,
      'fareHopAdult': 0,
      'fareHopChild': 0,
      'fareHopTertiary': 0,
      'from': 'from',
      'isFastLeg': False,
      'isFirst': True,
      'isLast': False,
      'legGeometry': {'length': 33,
       'points': 'jg|_F_oui`@c@OTqAs@WV_BJUI[BSDBNCf@e@Ai@\\w@LK]q@HmA@q@CeBt@XDFHO'},
      'messages': [],
      'mode': 'WALK',
      'startLat': '-36.8499741',
      'startLon': '174.7686468',
      'startTime': '2017-07-14T08:22',
      'startTimeStr': '8:22 am',
      'stops': [],
      'to': 'Symonds St and Waterloo, 2 Symonds St',
      'toStr': 'Walk to Symonds St and Waterloo, 2 Symonds St'},
     {'distance': 'undefined metres',
      'distanceStr': 'undefined metres',
      'duration': 1560000,
      'durationStr': '26 min',
      'endLat': '-36.89895',
      'endLon': '174.8125',
      'endTime': '2017-07-14T08:55',
      'endTimeStr': '8:55 am',
      'fareAdult': 550,
      'fareChild': 300,
      'fareHopAdult': 0,
      'fareHopChild': 0,
      'fareHopTertiary': 0,
      'from': 'Symonds St and Waterloo, 2 Symonds St',
      'isFastLeg': False,
      'isFirst': False,
      'isLast': False,
      'legGeometry': {'length': 353,
       'points': '~j|_Fydvi`@IJDHbF|GtBzBbJbJhCxB^`@FKGJtErFzFlGhCfDHMILX\\PNx@b@La@rK}SJa@rBcONe@VW~@[BACKBJpHuBbCm@T?PBdKzBt@cGICHBdF{`@ACGAHBf@}D|@XTBf@CVBxQ~DBOCN~@NbGKBQ?P~JW?Q?P`BCd@w@`JeMGIFHnIoLEIDHn@}@TYtO{LNMCIBHbGwEdJoJGIFHpFyFCIBHjAqAh@q@tEcHz@sAAI@HpE{GfC_EGMFLlAkBlCyDvJaOKKJJhDcFDUHSr@s@j@_BNi@~EaSPu@ECDBtCiLCCBBT{@N{@@k@Gg@gB{HA]Fa@HKNAlEp@TGFGxCsLICFD|BkJGC'},
      'messages': [],
      'mode': 'BUS',
      'provider': 'HE',
      'routeCode': '500',
      'routeName': 'Britomart To Botany Town Centre Via Ti Rakau Dr',
      'startLat': '-36.85056',
      'startLon': '174.77213',
      'startTime': '2017-07-14T08:29',
      'startTimeStr': '8:29 am',
      'stops': [{'code': '7144',
        'geometry': {'data': [-36.85056, 174.77213],
         'encoded': False,
         'type': 'point'}},
       {'code': '7152',
        'geometry': {'data': [-36.85494, 174.76747],
         'encoded': False,
         'type': 'point'}},
       {'code': '7156',
        'geometry': {'data': [-36.85797, 174.76407],
         'encoded': False,
         'type': 'point'}},
       {'code': '1028',
        'geometry': {'data': [-36.86168, 174.77038],
         'encoded': False,
         'type': 'point'}},
       {'code': '7178',
        'geometry': {'data': [-36.86626, 174.77182],
         'encoded': False,
         'type': 'point'}},
       {'code': '7180',
        'geometry': {'data': [-36.86741, 174.77725],
         'encoded': False,
         'type': 'point'}},
       {'code': '7206',
        'geometry': {'data': [-36.87143, 174.77715],
         'encoded': False,
         'type': 'point'}},
       {'code': '7208',
        'geometry': {'data': [-36.87304, 174.77714],
         'encoded': False,
         'type': 'point'}},
       {'code': '7210',
        'geometry': {'data': [-36.87497, 174.77726],
         'encoded': False,
         'type': 'point'}},
       {'code': '7600',
        'geometry': {'data': [-36.87738, 174.77979],
         'encoded': False,
         'type': 'point'}},
       {'code': '7602',
        'geometry': {'data': [-36.87907, 174.78195],
         'encoded': False,
         'type': 'point'}},
       {'code': '7604',
        'geometry': {'data': [-36.88218, 174.78468],
         'encoded': False,
         'type': 'point'}},
       {'code': '7606',
        'geometry': {'data': [-36.88525, 174.7876],
         'encoded': False,
         'type': 'point'}},
       {'code': '7608',
        'geometry': {'data': [-36.88647, 174.78885],
         'encoded': False,
         'type': 'point'}},
       {'code': '7610',
        'geometry': {'data': [-36.88844, 174.79139],
         'encoded': False,
         'type': 'point'}},
       {'code': '7612',
        'geometry': {'data': [-36.89014, 174.79379],
         'encoded': False,
         'type': 'point'}},
       {'code': '7616',
        'geometry': {'data': [-36.89311, 174.79782],
         'encoded': False,
         'type': 'point'}},
       {'code': '7500',
        'geometry': {'data': [-36.89584, 174.80356],
         'encoded': False,
         'type': 'point'}},
       {'code': '7502',
        'geometry': {'data': [-36.89659, 174.80569],
         'encoded': False,
         'type': 'point'}},
       {'code': '7504',
        'geometry': {'data': [-36.89832, 174.81069],
         'encoded': False,
         'type': 'point'}},
       {'code': '7506',
        'geometry': {'data': [-36.89895, 174.8125],
         'encoded': False,
         'type': 'point'}}],
      'to': 'Main Hwy and Amy, 176 Main Hwy',
      'toStr': 'Bus to Main Hwy and Amy, 176 Main Hwy',
      'tripId': '5500073031-20170705140526_v55.10'},
     {'distance': '526 metres',
      'distanceExact': 526,
      'distanceStr': '526 metres',
      'duration': 480000,
      'durationStr': '8 min',
      'endLat': '-36.8954602',
      'endLon': '174.815197',
      'endTime': '2017-07-14T09:03',
      'endTimeStr': '9:03 am',
      'fareAdult': 0,
      'fareChild': 0,
      'fareHopAdult': 0,
      'fareHopChild': 0,
      'fareHopTertiary': 0,
      'from': 'Main Hwy and Amy, 176 Main Hwy',
      'isFastLeg': False,
      'isFirst': False,
      'isLast': True,
      'legGeometry': {'length': 13, 'points': 'lye`Fca~i`@HDT{@sVuKZmA'},
      'messages': [],
      'mode': 'WALK',
      'startLat': '-36.89895',
      'startLon': '174.8125',
      'startTime': '2017-07-14T08:55',
      'startTimeStr': '8:55 am',
      'stops': [],
      'to': 'to',
      'toStr': 'Walk to to'}],
    'startTime': '2017-07-14T08:22',
    'startTimeStr': '8:22 am'},
   {'duration': 2460000,
    'durationStr': '41 minutes',
    'endTime': '2017-07-14T09:05',
    'endTimeStr': '9:05 am',
    'fareAdult': 550,
    'fareChild': 300,
    'fareError': None,
    'fareHopAdult': 315,
    'fareHopChild': 180,
    'fareHopTertiary': 235,
    'fareWarnings': {},
    'legs': [{'distance': '435 metres',
      'distanceExact': 435,
      'distanceStr': '435 metres',
      'duration': 420000,
      'durationStr': '7 min',
      'endLat': '-36.85056',
      'endLon': '174.77213',
      'endTime': '2017-07-14T08:31',
      'endTimeStr': '8:31 am',
      'fareAdult': 0,
      'fareChild': 0,
      'fareHopAdult': 0,
      'fareHopChild': 0,
      'fareHopTertiary': 0,
      'from': 'from',
      'isFastLeg': False,
      'isFirst': True,
      'isLast': False,
      'legGeometry': {'length': 33,
       'points': 'jg|_F_oui`@c@OTqAs@WV_BJUI[BSDBNCf@e@Ai@\\w@LK]q@HmA@q@CeBt@XDFHO'},
      'messages': [],
      'mode': 'WALK',
      'startLat': '-36.8499741',
      'startLon': '174.7686468',
      'startTime': '2017-07-14T08:24',
      'startTimeStr': '8:24 am',
      'stops': [],
      'to': 'Symonds St and Waterloo, 2 Symonds St',
      'toStr': 'Walk to Symonds St and Waterloo, 2 Symonds St'},
     {'distance': 'undefined metres',
      'distanceStr': 'undefined metres',
      'duration': 1560000,
      'durationStr': '26 min',
      'endLat': '-36.89895',
      'endLon': '174.8125',
      'endTime': '2017-07-14T08:57',
      'endTimeStr': '8:57 am',
      'fareAdult': 550,
      'fareChild': 300,
      'fareHopAdult': 0,
      'fareHopChild': 0,
      'fareHopTertiary': 0,
      'from': 'Symonds St and Waterloo, 2 Symonds St',
      'isFastLeg': False,
      'isFirst': False,
      'isLast': False,
      'legGeometry': {'length': 349,
       'points': '~j|_Fydvi`@IJDHbF|GtBzBbJbJhCxB^`@FKGJtErFzFlGhCfDHMILX\\PNx@b@La@rK}SJa@rBcONe@VW~@[BACKBJpHuBbCm@T?PBdKzBt@cGICHBdF{`@ACGAHBf@}D|@XTBf@CVBxQ~DBOCN~@NfSc@?Q?P`BCd@w@`JeMGIFHnIoLEIDHn@}@TYtO{LNMCIBHbGwEdJoJGIFHpFyFCIBHjAqAh@q@tEcHz@sAAI@HpE{GfC_EGMFLlAkBlCyDvJaOKKJJhDcFDUHSr@s@j@_BNi@~EaSPu@ECDBtCiLCCBBT{@N{@@k@Gg@gB{HA]Fa@HKNAlEp@TGFGxCsLICFD|BkJGC'},
      'messages': [],
      'mode': 'BUS',
      'provider': 'HE',
      'routeCode': '550',
      'routeName': 'Britomart To Cockle Bay Via Newmarket',
      'startLat': '-36.85056',
      'startLon': '174.77213',
      'startTime': '2017-07-14T08:31',
      'startTimeStr': '8:31 am',
      'stops': [{'code': '7144',
        'geometry': {'data': [-36.85056, 174.77213],
         'encoded': False,
         'type': 'point'}},
       {'code': '7152',
        'geometry': {'data': [-36.85494, 174.76747],
         'encoded': False,
         'type': 'point'}},
       {'code': '7156',
        'geometry': {'data': [-36.85797, 174.76407],
         'encoded': False,
         'type': 'point'}},
       {'code': '1028',
        'geometry': {'data': [-36.86168, 174.77038],
         'encoded': False,
         'type': 'point'}},
       {'code': '7178',
        'geometry': {'data': [-36.86626, 174.77182],
         'encoded': False,
         'type': 'point'}},
       {'code': '7180',
        'geometry': {'data': [-36.86741, 174.77725],
         'encoded': False,
         'type': 'point'}},
       {'code': '7206',
        'geometry': {'data': [-36.87143, 174.77715],
         'encoded': False,
         'type': 'point'}},
       {'code': '7210',
        'geometry': {'data': [-36.87497, 174.77726],
         'encoded': False,
         'type': 'point'}},
       {'code': '7600',
        'geometry': {'data': [-36.87738, 174.77979],
         'encoded': False,
         'type': 'point'}},
       {'code': '7602',
        'geometry': {'data': [-36.87907, 174.78195],
         'encoded': False,
         'type': 'point'}},
       {'code': '7604',
        'geometry': {'data': [-36.88218, 174.78468],
         'encoded': False,
         'type': 'point'}},
       {'code': '7606',
        'geometry': {'data': [-36.88525, 174.7876],
         'encoded': False,
         'type': 'point'}},
       {'code': '7608',
        'geometry': {'data': [-36.88647, 174.78885],
         'encoded': False,
         'type': 'point'}},
       {'code': '7610',
        'geometry': {'data': [-36.88844, 174.79139],
         'encoded': False,
         'type': 'point'}},
       {'code': '7612',
        'geometry': {'data': [-36.89014, 174.79379],
         'encoded': False,
         'type': 'point'}},
       {'code': '7616',
        'geometry': {'data': [-36.89311, 174.79782],
         'encoded': False,
         'type': 'point'}},
       {'code': '7500',
        'geometry': {'data': [-36.89584, 174.80356],
         'encoded': False,
         'type': 'point'}},
       {'code': '7502',
        'geometry': {'data': [-36.89659, 174.80569],
         'encoded': False,
         'type': 'point'}},
       {'code': '7504',
        'geometry': {'data': [-36.89832, 174.81069],
         'encoded': False,
         'type': 'point'}},
       {'code': '7506',
        'geometry': {'data': [-36.89895, 174.8125],
         'encoded': False,
         'type': 'point'}}],
      'to': 'Main Hwy and Amy, 176 Main Hwy',
      'toStr': 'Bus to Main Hwy and Amy, 176 Main Hwy',
      'tripId': '5550011200-20170705140526_v55.10'},
     {'distance': '526 metres',
      'distanceExact': 526,
      'distanceStr': '526 metres',
      'duration': 480000,
      'durationStr': '8 min',
      'endLat': '-36.8954602',
      'endLon': '174.815197',
      'endTime': '2017-07-14T09:05',
      'endTimeStr': '9:05 am',
      'fareAdult': 0,
      'fareChild': 0,
      'fareHopAdult': 0,
      'fareHopChild': 0,
      'fareHopTertiary': 0,
      'from': 'Main Hwy and Amy, 176 Main Hwy',
      'isFastLeg': False,
      'isFirst': False,
      'isLast': True,
      'legGeometry': {'length': 13, 'points': 'lye`Fca~i`@HDT{@sVuKZmA'},
      'messages': [],
      'mode': 'WALK',
      'startLat': '-36.89895',
      'startLon': '174.8125',
      'startTime': '2017-07-14T08:57',
      'startTimeStr': '8:57 am',
      'stops': [],
      'to': 'to',
      'toStr': 'Walk to to'}],
    'startTime': '2017-07-14T08:24',
    'startTimeStr': '8:24 am'},
   {'duration': 2040000,
    'durationStr': '34 minutes',
    'endTime': '2017-07-14T09:08',
    'endTimeStr': '9:08 am',
    'fareAdult': 550,
    'fareChild': 300,
    'fareError': None,
    'fareHopAdult': 315,
    'fareHopChild': 180,
    'fareHopTertiary': 235,
    'fareWarnings': {},
    'legs': [{'distance': '435 metres',
      'distanceExact': 435,
      'distanceStr': '435 metres',
      'duration': 420000,
      'durationStr': '7 min',
      'endLat': '-36.85056',
      'endLon': '174.77213',
      'endTime': '2017-07-14T08:41',
      'endTimeStr': '8:41 am',
      'fareAdult': 0,
      'fareChild': 0,
      'fareHopAdult': 0,
      'fareHopChild': 0,
      'fareHopTertiary': 0,
      'from': 'from',
      'isFastLeg': False,
      'isFirst': True,
      'isLast': False,
      'legGeometry': {'length': 33,
       'points': 'jg|_F_oui`@c@OTqAs@WV_BJUI[BSDBNCf@e@Ai@\\w@LK]q@HmA@q@CeBt@XDFHO'},
      'messages': [],
      'mode': 'WALK',
      'startLat': '-36.8499741',
      'startLon': '174.7686468',
      'startTime': '2017-07-14T08:34',
      'startTimeStr': '8:34 am',
      'stops': [],
      'to': 'Symonds St and Waterloo, 2 Symonds St',
      'toStr': 'Walk to Symonds St and Waterloo, 2 Symonds St'},
     {'distance': 'undefined metres',
      'distanceStr': 'undefined metres',
      'duration': 1500000,
      'durationStr': '25 min',
      'endLat': '-36.89615',
      'endLon': '174.81431',
      'endTime': '2017-07-14T09:06',
      'endTimeStr': '9:06 am',
      'fareAdult': 550,
      'fareChild': 300,
      'fareHopAdult': 0,
      'fareHopChild': 0,
      'fareHopTertiary': 0,
      'from': 'Symonds St and Waterloo, 2 Symonds St',
      'isFastLeg': False,
      'isFirst': False,
      'isLast': False,
      'legGeometry': {'length': 356,
       'points': '~j|_Fydvi`@IJhFfHzB`CbK`KlB~ADKEJFH|G`I`ElEhCfDHMILX\\PNx@b@La@rK}SJa@rBcONe@VW~@[BACKBJlA_@jCq@VK~@WbCm@T?PBdKzBb@_D?MICHBvFsc@ICHBf@}D|@XTBf@CVBdPnDBOCNjAVf@FbGKBQ?P~JW?Q?P`BCd@w@`JeMGIFHnIoLEIDHn@}@TYtO{LNMCIBHbGwEdJoJGIFHpFyFCIBHjAqAh@q@tEcHz@sAAI@HpE{GfC_EGMFLlAkBlCyDvJaOKKJJhDcFDUHSr@s@j@_BNi@~EaSPu@ECDBtCiLCCBBT{@N{@@k@Gg@gB{HA]Fa@HKNAlEp@TGFGxCsLICFD|BkJGCHBR{@qBw@wMaGCF'},
      'messages': [],
      'mode': 'BUS',
      'provider': 'NZBML',
      'routeCode': '595',
      'routeName': 'Britomart To Glen Innes Via Panmure',
      'startLat': '-36.85056',
      'startLon': '174.77213',
      'startTime': '2017-07-14T08:41',
      'startTimeStr': '8:41 am',
      'stops': [{'code': '7144',
        'geometry': {'data': [-36.85056, 174.77213],
         'encoded': False,
         'type': 'point'}},
       {'code': '7150',
        'geometry': {'data': [-36.85482, 174.76759],
         'encoded': False,
         'type': 'point'}},
       {'code': '7156',
        'geometry': {'data': [-36.85797, 174.76407],
         'encoded': False,
         'type': 'point'}},
       {'code': '1028',
        'geometry': {'data': [-36.86168, 174.77038],
         'encoded': False,
         'type': 'point'}},
       {'code': '7176',
        'geometry': {'data': [-36.86617, 174.77139],
         'encoded': False,
         'type': 'point'}},
       {'code': '7180',
        'geometry': {'data': [-36.86741, 174.77725],
         'encoded': False,
         'type': 'point'}},
       {'code': '7204',
        'geometry': {'data': [-36.87116, 174.77723],
         'encoded': False,
         'type': 'point'}},
       {'code': '7208',
        'geometry': {'data': [-36.87304, 174.77714],
         'encoded': False,
         'type': 'point'}},
       {'code': '7210',
        'geometry': {'data': [-36.87497, 174.77726],
         'encoded': False,
         'type': 'point'}},
       {'code': '7600',
        'geometry': {'data': [-36.87738, 174.77979],
         'encoded': False,
         'type': 'point'}},
       {'code': '7602',
        'geometry': {'data': [-36.87907, 174.78195],
         'encoded': False,
         'type': 'point'}},
       {'code': '7604',
        'geometry': {'data': [-36.88218, 174.78468],
         'encoded': False,
         'type': 'point'}},
       {'code': '7606',
        'geometry': {'data': [-36.88525, 174.7876],
         'encoded': False,
         'type': 'point'}},
       {'code': '7608',
        'geometry': {'data': [-36.88647, 174.78885],
         'encoded': False,
         'type': 'point'}},
       {'code': '7610',
        'geometry': {'data': [-36.88844, 174.79139],
         'encoded': False,
         'type': 'point'}},
       {'code': '7612',
        'geometry': {'data': [-36.89014, 174.79379],
         'encoded': False,
         'type': 'point'}},
       {'code': '7616',
        'geometry': {'data': [-36.89311, 174.79782],
         'encoded': False,
         'type': 'point'}},
       {'code': '7500',
        'geometry': {'data': [-36.89584, 174.80356],
         'encoded': False,
         'type': 'point'}},
       {'code': '7502',
        'geometry': {'data': [-36.89659, 174.80569],
         'encoded': False,
         'type': 'point'}},
       {'code': '7504',
        'geometry': {'data': [-36.89832, 174.81069],
         'encoded': False,
         'type': 'point'}},
       {'code': '7506',
        'geometry': {'data': [-36.89895, 174.8125],
         'encoded': False,
         'type': 'point'}},
       {'code': '7456',
        'geometry': {'data': [-36.89615, 174.81431],
         'encoded': False,
         'type': 'point'}}],
      'to': 'Amy St and Ballin, 29 Amy St',
      'toStr': 'Bus to Amy St and Ballin, 29 Amy St',
      'tripId': '14595039979-20170705140526_v55.10'},
     {'distance': '142 metres',
      'distanceExact': 142,
      'distanceStr': '142 metres',
      'duration': 120000,
      'durationStr': '1 min',
      'endLat': '-36.8954602',
      'endLon': '174.815197',
      'endTime': '2017-07-14T09:08',
      'endTimeStr': '9:08 am',
      'fareAdult': 0,
      'fareChild': 0,
      'fareHopAdult': 0,
      'fareHopChild': 0,
      'fareHopTertiary': 0,
      'from': 'Amy St and Ballin, 29 Amy St',
      'isFastLeg': False,
      'isFirst': False,
      'isLast': True,
      'legGeometry': {'length': 5, 'points': '|ge`Fml~i`@BEgD{AZmA'},
      'messages': [],
      'mode': 'WALK',
      'startLat': '-36.89615',
      'startLon': '174.81431',
      'startTime': '2017-07-14T09:06',
      'startTimeStr': '9:06 am',
      'stops': [],
      'to': 'to',
      'toStr': 'Walk to to'}],
    'startTime': '2017-07-14T08:34',
    'startTimeStr': '8:34 am'}]},
 'status': 'OK'}

In [7]:
regions = ['auckland']#, 'wellington']
departure_time = '2017-07-13T07:30:00'
for region in regions:
    rp = hp.get_data('rental_points', region)
    g = collect_fares(rp, departure_time, region)

    path = hp.get_path('transit_costs', region)
    g.to_csv(path, index=False)
    print('* ', region)
    print(g.head())


This will take about 570.360000 minutes
*  auckland
  orig_name                   dest_name  card_fare
0    Albany                    Avondale       6.10
1    Albany                    Balmoral       6.10
2    Albany         Beachhaven/Birkdale       3.15
3    Albany  Blockhouse Bay/New Windsor       6.10
4    Albany                Botony Downs       7.50

In [9]:
# Fill some holes by trying again

def get_transit_costs_with_coords(region):
    f = hp.get_data('transit_costs', region)
    g = hp.get_data('rental_points', region)
    g['coords'] = g['geometry'].map(lambda g: g.coords[0])
    f = f.merge(g[['rental_area', 'coords']].rename(columns={
        'rental_area': 'orig_name',
        'coords': 'orig_coords',
    }))
    f = f.merge(g[['rental_area', 'coords']].rename(columns={
        'rental_area': 'dest_name',
        'coords': 'dest_coords',
    }))
    return f.sort_values(['orig_name', 'dest_name'])

def fill_holes(region, departure_time=None):
    f = get_transit_costs_with_coords(region)
    cond = f['card_fare'].isnull()
    
    if region == 'auckland':
        get_journey = get_journey_auckland
        get_fare = get_fare_auckland
        time_per_call = 3.6
    elif region == 'wellington':
        get_journey = get_journey_wellington
        get_fare = get_fare_wellington
        time_per_call = 2.4
    
    print('This will take about {:02f} minutes'.format(f[cond].shape[0]*time_per_call/60))
        
    new_rows = []
    for __, row in f[cond].iterrows():
        try:
            journey = get_journey(row['orig_coords'], row['dest_coords'], departure_time=departure_time)
            fare = get_fare(journey)
        except:
            fare = np.nan
        new_rows.append([row['orig_name'], row['dest_name'], fare])
    g = pd.DataFrame(new_rows, columns=['orig_name', 'dest_name', 'card_fare']).fillna(value=np.nan)

    f = f[~cond].drop(['orig_coords', 'dest_coords'], axis=1)
    f = pd.concat([f, g]).sort_values(['orig_name', 'dest_name'])
    return f

In [20]:
departure_time = '2017-07-13T07:30:00'
for region in ['auckland']:#, 'wellington']:
    f = fill_holes(region, departure_time=departure_time)
    path = hp.get_path('transit_costs', region)
    path = path.parent/(path.name + '.new')
    f.to_csv(path, index=False)


This will take about 68.040000 minutes

In [29]:
region = 'auckland'
path1 = hp.get_path('transit_costs', region)
f1 = pd.read_csv(path1)
path2 = path1.parent/(path1.name + '.new')
f2 = pd.read_csv(path2)
print(f1.shape[0], f1[f1['card_fare'].isnull()].shape[0])
print(f2.shape[0], f2[f2['card_fare'].isnull()].shape[0])

if f2.shape[0] < f1.shape[0]:
    # Overwrite old costs with new
    path2.rename(path1)
    path2.unlink()


9506 1134
9506 1134

Canterbury has no fare calculator API. So estimate Canterbury fares from fare zones and fare table.


In [ ]:
# Fares
fares = pd.DataFrame([[1, 2.55], [2, 3.75]], columns=['#zones_traveled', 'card_fare'])
fares

# Zones
path = DATA_DIR/'processed'/'canterbury'/'fare_zones.geojson'
zones = gpd.read_file(str(path))
zones

# Attach zones to rental points
rp = hp.get_data('rental_points', 'canterbury')
g = gpd.sjoin(rp, zones, op='within')
g = g[['rental_area', 'zone']].copy()
g.head()

# Compute origin and destination zones
f = hp.get_data('commutes_transit', 'canterbury')
f = f.merge(g.rename(columns={'rental_area': 'orig_name', 'zone': 'orig_zone'}))
f = f.merge(g.rename(columns={'rental_area': 'dest_name', 'zone': 'dest_zone'}))

# Compute #zones traveled, then card fare
f['#zones_traveled'] = abs(f['orig_zone'] - f['dest_zone']) + 1
f = f.merge(fares)

# Cut down and save
f = f[['orig_name', 'dest_name', 'card_fare']].copy()
path = hp.get_path('transit_costs', 'canterbury')
f.to_csv(path, index=False)
f

Compile roundtrip commute costs and durations and save to JSON for web


In [ ]:
def build_json_commute_costs(region):
    """
    Consolidate the data in the commute CSV files for this region into 
    one JSON-compatibel dictionary of roundtrip commute costs and durations. 
    More specifically, return a dictionary of the form 
    ``{'index_by_name': index_by_name, 'matrix': M}``, where 
    ``index_by_name`` is a dictionary of the form
    rental area name -> row/column index in the lower-triangular half-matrix 
    ``M``, where ``M`` is encoded by a dictionary of the form
    mode -> list of lists of cost-duration pairs 
    such that ``M[mode][i][j]`` equals the cost in dollars
    and the duration in hours that it takes to travel roundtrip by the 
    given mode from the rental area point with index ``i >= 0`` 
    to the rental area point with index ``j <= i``.
    """
    # Get rental area names
    rents = hp.get_data('rents', region)
    names = sorted(rents['rental_area'].unique())
    index_by_name = {name: i for (i, name) in enumerate(names)}
    n = len(names)

    # Add cost info to commutes data
    frames = []
    for mode in hp.MODES:
        f = hp.get_data('commutes_' + mode, region)
        f['mode'] = mode
        f['orig_index'] = f['orig_name'].map(index_by_name)
        f['dest_index'] = f['dest_name'].map(index_by_name)
        
        # Convert from meters to kilometers and seconds to hours
        f['distance'] /= 1000
        f['duration'] /= 3600

        # Compute costs
        if mode == 'transit':
            # Use separate cost table
            costs = hp.get_data('transit_costs', region).rename(
              columns={'card_fare': 'cost'})
            f = f.merge(costs)
            # Nullify costs with missing distances
            cond = f['distance'].isnull()
            f.loc[cond, 'cost'] = np.nan
        else:
            # Multiply distance by cost per distance
            f['cost'] = hp.COST_BY_MODE[mode]*f['distance']

        # Insert zero costs for area-to-self commutes
        g = pd.DataFrame()
        g['orig_index'] = f['orig_index'].unique()
        g['dest_index'] = f['orig_index'].unique()
        g['mode'] = mode
        g['cost'] = 0
        g['duration'] = 0
        f = pd.concat([f, g])
        frames.append(f[['mode', 'orig_index', 'dest_index', 'cost', 'duration']].copy())

    f = pd.concat(frames)
    
    # Make a dictionary M of the form mode -> [[(cost, duration)]], 
    # where the double list is indexed by rental areas
    M = {mode: [[(None, None) for j in range(n)] for i in range(n)] 
      for mode in hp.MODES}
    f = hp.nan_to_none(f)
    for mode, oi, di, cost, duration in f.itertuples(index=False):
        M[mode][oi][di] = (cost, duration)
    
    # Make a dictionary MM of the form mode -> [[(roundtrip cost, roundtrip duration)]]
    # where the double list is indexed by rental areas i, j with i < j
    MM = {mode: [[(None, None) for j in range(i + 1)] for i in range(n)] 
      for mode in hp.MODES}
    for mode in hp.MODES:
        for i in range(n):
            for j in range(i + 1):
                try:
                    cost = round(M[mode][i][j][0] + M[mode][j][i][0], 2)
                    duration = round(M[mode][i][j][1] + M[mode][j][i][1], 2)
                    MM[mode][i][j] = (cost, duration)
                except TypeError:
                    # Defaults to MM[mode][i][j] = (None, None) 
                    pass  
    
    data = {'index_by_name': index_by_name, 'matrix': MM}
    return data

for region in hp.REGIONS:
    d = build_json_commute_costs(region)
    path = hp.get_path('commute_costs', region)   
    with path.open('w') as tgt:
        json.dump(d, tgt)