├── .gitignore ├── README.md ├── license.txt ├── requirements.txt ├── setup.py └── transitflow ├── __init__.py ├── assets ├── calendar_icon.png └── clock_icon.png ├── concatenate_csvs.py ├── templates └── template.pde ├── transitflow.py └── transitland_api.py /.gitignore: -------------------------------------------------------------------------------- 1 | # https://github.com/github/gitignore/blob/master/Python.gitignore 2 | *.csv.icloud 3 | *.tiff 4 | *.DS_Store 5 | *ffmpeg.txt 6 | *.mp4 7 | *.mp4.icloud 8 | *.m4v 9 | *.mov 10 | *.mov.icloud 11 | virtualenv/ 12 | venv/ 13 | census/ 14 | sketches/ 15 | examples/ 16 | notebooks/ 17 | archive/ 18 | blog/ 19 | blogpost.md 20 | transitflow_paris.py 21 | 22 | # Byte-compiled / optimized / DLL files 23 | __pycache__/ 24 | *.py[cod] 25 | 26 | # C extensions 27 | *.so 28 | 29 | # Distribution / packaging 30 | .Python 31 | env/ 32 | build/ 33 | develop-eggs/ 34 | dist/ 35 | downloads/ 36 | eggs/ 37 | .eggs/ 38 | lib/ 39 | lib64/ 40 | parts/ 41 | sdist/ 42 | var/ 43 | *.egg-info/ 44 | .installed.cfg 45 | *.egg 46 | 47 | # PyInstaller 48 | # Usually these files are written by a python script from a template 49 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 50 | *.manifest 51 | *.spec 52 | 53 | # Installer logs 54 | pip-log.txt 55 | pip-delete-this-directory.txt 56 | 57 | # Unit test / coverage reports 58 | htmlcov/ 59 | .tox/ 60 | .coverage 61 | .coverage.* 62 | .cache 63 | nosetests.xml 64 | coverage.xml 65 | *,cover 66 | 67 | # Translations 68 | *.mo 69 | *.pot 70 | 71 | # Django stuff: 72 | *.log 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | target/ 79 | *.pde 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Visualizing scheduled transit frequency 2 | 3 | *TransitFlow* uses Mapzen's [Transitland API](https://transit.land/) to download transit schedule data and [Processing](https://processing.org/) with [Unfolding Maps](http://unfoldingmaps.org/) to animate scheduled transit frequency. 4 | 5 | *TransitFlow* was created by [Will Geary](https://twitter.com/wgeary) during an internship at Mapzen in 2017. See this blog post for more info: https://mapzen.com/blog/animating-transitland/ 6 | 7 | Here is an example animation generated for San Francisco with a single command: 8 | 9 | `python transitflow.py --name=san_francisco --bbox=-122.515411,37.710714,-122.349243,37.853983 --clip_to_bbox` 10 | 11 | [![IMAGE ALT TEXT](http://i.imgur.com/3zF4uE7.png)](https://vimeo.com/230827684?quality=1080p "San Francisco Transit Flows") 12 | 13 | See here for more [transit flow visualizations](https://vimeopro.com/willgeary/transit-flows). 14 | 15 | ## Install Processing 16 | 1. Download [Processing 3](https://processing.org/). 17 | 2. Download [Unfolding Maps version 0.9.9 for Processing 3](http://services.informatik.hs-mannheim.de/~nagel/GDV/Unfolding_for_processing_0.9.9beta.zip). 18 | 3. Navigate to `~/Documents/Processing/libraries` on your machine. 19 | 4. Drag and drop the unzipped Unfolding Maps folder into `~/Documents/Processing/libraries`. 20 | 5. Open Processing, navigate to Sketch > Import Library > Add Libary. Search for "Video Export" and click Install. 21 | 6. Quit and re-open Processing. 22 | 23 | ## Instructions 24 | - Download the repository, unzip it and `cd` into it 25 | - `pip install -r requirements.txt` to install the python requirements (pandas, numpy, requests) 26 | - `cd transitflow` 27 | 28 | Now, you are ready to download transit schedule data and generate visualizations. 29 | 30 | There are two ways to go about using this tool: 31 | 32 | ### 1) Search by transit operator Onestop ID 33 | 34 | You can visualize a single transit operator by passing in the operator's Onestop ID. What's a Onestop ID, you ask? As part of Transitland's [Onestop ID Scheme](https://transit.land/documentation/onestop-id-scheme/), every transit operator, route, feed and stop are assigned a unique identifier called a Onestop ID. 35 | 36 | You can look up an operator's Onestop ID using the [Transitland Feed Registery](https://transit.land/feed-registry/). For example, the Onestop ID for San Francisco BART is `o-9q9-bart`. 37 | 38 | Visualize one day of BART transit flows: 39 | 40 | - `python transitflow.py --name=bart --operator=o-9q9-bart` 41 | 42 | [![IMAGE ALT TEXT](http://i.imgur.com/NFPEnYj.png)](https://vimeo.com/230364702?quality=1080p "One Day of BART Trips") 43 | 44 | ### 2) Search by bounding box 45 | 46 | You can also visualize transit flows by searching for all operators within a bounding box. The bounding box must be in the format: West, South, East, North. I like using [bboxfinder](http://bboxfinder.com/) to draw bounding boxes. For example, here's the command to visualize transit flows in Chicago: 47 | 48 | - `python transitflow.py --name=chicago --bbox=-87.992249,41.605175,-87.302856,42.126747 --clip_to_bbox --exclude=o-9-amtrak,o-9-amtrakcharteredvehicle` 49 | 50 | [![IMAGE ALT TEXT](http://i.imgur.com/pH7AwgB.png)](https://vimeo.com/230857619?quality=1080p "Chicago Transit Flows") 51 | 52 | Note, the use of `--clip_to_bbox`. This command will clip the dataset to only include transit vehicle trips within the specified bounding box, both in the geo-visualization and in the vehicle count calculations that drive the stacked bar chart. 53 | 54 | Also, note the optional use of `--exclude`. This command will exclude specified operators, Amtrak in this case. 55 | 56 | ### Play your animation 57 | 58 | Navigate to `sketches\{name}\{date}\sketch` and open the `sketch.pde` file. 59 | 60 | This should open the Processing application. Simply click Play or `command + r` to play the animation. 61 | 62 | ### Change map providers 63 | 64 | Cycle through the first two rows on the keyboard (1 to 0, q to u) to see the built in map provider options. 65 | 66 | Read more about Unfolding Maps map providers here: http://unfoldingmaps.org/tutorials/mapprovider-and-tiles.html 67 | 68 | ### Panning and zooming 69 | 70 | You can pan around on the map by clicking and dragging it. You can zoom in with Shift + "+" and zoom out with "-". 71 | 72 | ### Exporting to video 73 | 74 | Open `sketch.pde` file. 75 | 76 | - For a quick, medium quality video, set `boolean recording = true;` 77 | - For a high quality video, set `boolean recording = true;` and `boolean HQ = true;`. This will generate 3,600 .tiff frames that can be stiched together using ffmpeg or Processing's built in movie maker tool. 78 | 79 | ## Command line arguments 80 | 81 | **Key**|**Status**|**Description**|**Example** 82 | -----|-----|-----|----- 83 | --name|required|The name of your project|--name=boston 84 | --date|optional|Defaults to today's date|--date=2017-08-15 85 | --operator|optional|Operator Onestop ID|--operator=o-drt-mbta 86 | --bbox|optional|West, South, East, North| --bbox=-71.4811,42.1135,-70.6709,42.6157 87 | --clip\_to\_bbox|optional|Clip results to bounding box|--clip\_to\_bbox 88 | --exclude|optional|Operators to be excluded|--exclude=o-9-amtrak 89 | --apikey|optional|Mapzen API key|--apikey=mapzen-abc1234 90 | 91 | A Mapzen API Key is optional, but recommended for faster results. Sign up for a [Mapzen API Key here](https://mapzen.com/developers/sign_up). 92 | 93 | ## Troubleshooting 94 | 95 | If your visualization is not working as expected... 96 | 97 | - Make sure that the operator of interest actually has service on the specified date (no `--date` argument defaults to today's date). Some operators are better than others at sharing updated data. For example, Mexico City's [Metrobús](https://transit.land/api/v1/schedule_stop_pairs?operator_onestop_id=o-9g3w-metrobs) has a `service_end_date` of 2016-08-17. So, you would need pass in a `--date=2016-08-17` or earlier or to download and visualize the Metrobús. 98 | 99 | - Make sure that Transitland has coverage in your area of interest. You can search for transit operators and feeds using the [Transitland Feed Registery](https://transit.land/feed-registry/). Is Transitland missing a feed? [Let us know](https://transit.land/participate/)! 100 | 101 | ## Attribution 102 | - [Will Geary](https://twitter.com/wgeary) for Mapzen, August 2017 103 | - Transit schedule data from [Mapzen](https://mapzen.com/), [Transitland](https://transit.land/) 104 | - Map tiles from [Stamen](https://stamen.com/), [Carto](http://carto.com/), [OpenStreetMap](http://www.openstreetmap.org/), [ESRI](http://www.esri.com/), [Bing Maps](https://www.bing.com/maps) 105 | - The visualization incorporates Processing code from [this workshop](https://github.com/juanfrans-courses/DataScienceSocietyWorkshop) by [Juan Francisco Saldarriaga](http://juanfrans.com/) to plot trips using linear interpolation. It also relies on the [Unfolding Maps](http://unfoldingmaps.org/) library by [Till Nagel](http://tillnagel.com/) for its built-in map tiles and functions to convert geolocations into screen positions. Thank you to Juan and Till for your inspiring work! 106 | 107 | ## Sources of inspiration 108 | - *[Shanghai Metro Flow](http://tillnagel.com/2013/12/shanghai-metro-flow/)*, Till Nagel 109 | - *[Barcelona Cycle Challenge](http://juanfrans.com/projects/barcelonaCycleChallenge.html)*, Juan Francisco Saldarriaga 110 | - *[Seven Days of Car-Sharing in Milan](http://labs.densitydesign.org/carsharing/)*, Matteo Azzi, Daniele Ciminieri, others 111 | - *[NYC Taxis: A Day in the Life](http://chriswhong.github.io/nyctaxi/)*, Chris Whong 112 | - *[Analyzing 1.1 Billion NYC Taxi and Uber Trips](http://toddwschneider.com/posts/analyzing-1-1-billion-nyc-taxi-and-uber-trips-with-a-vengeance/)*, Todd Schneider 113 | 114 | See more visualizations created with TransitFlow here: https://vimeopro.com/willgeary/transit-flows 115 | 116 | ## Press & Recognition 117 | 118 | - **The Guardian** [*"Go with the flow: the hypnotic beauty of public transport – mapped"*](https://www.theguardian.com/cities/2017/oct/04/hypnotic-beauty-public-transport-mapped) (October 2017) 119 | 120 | - **Le Monde** [*"L’étrange beauté des flux de Paris dans une carte animée"*](http://www.lemonde.fr/smart-cities/article/2017/10/12/l-etrange-beaute-des-flux-de-paris-dans-une-carte-animee_5200106_4811534.html#zqdvSAkUWw754WcX.99) (December 2017) 121 | 122 | - **Kantar Information is Beautiful Awards Longlist** [*"Multimodal Symphony"*](https://www.informationisbeautifulawards.com/showcase/2347) (October 2017) 123 | 124 | - **CityLab** [*"Mapping the Ebb and Flow of Transit Around the World"*](https://www.citylab.com/transportation/2017/09/visualize-transit-frequency-nearly-anywhere-in-the-world/538725/) (September 2017) 125 | 126 | - **Mobility Lab** [*"A visualized day of New York’s transit options, working together"*](https://mobilitylab.org/2017/04/11/nyc-visualization-transit-options/) (April 2017) 127 | 128 | - **Planetizen**[*"Watch Transit Move With These Animated Maps"*](https://www.planetizen.com/node/94598/watch-transit-move-these-animated-maps) (September 2017) 129 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Mapzen 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests>=2.18.1 2 | pandas>=0.20.3 3 | numpy>=1.13.1 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | import transitlandflows 4 | 5 | setup( 6 | name='transitlandflows', 7 | version='0.1', 8 | description='Transitland Flows: Visualization', 9 | author='Will Geary', 10 | author_email='willcgeary@gmail.com', 11 | url='https://github.com/transitland/transitland-processing-animation', 12 | license='License :: OSI Approved :: MIT License', 13 | packages=['transitlandflows'], 14 | zip_safe=False 15 | ) 16 | -------------------------------------------------------------------------------- /transitflow/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/transitland/transitland-processing-animation/bafe2f5c575e1b03471ae846ef95c350b57483b8/transitflow/__init__.py -------------------------------------------------------------------------------- /transitflow/assets/calendar_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/transitland/transitland-processing-animation/bafe2f5c575e1b03471ae846ef95c350b57483b8/transitflow/assets/calendar_icon.png -------------------------------------------------------------------------------- /transitflow/assets/clock_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/transitland/transitland-processing-animation/bafe2f5c575e1b03471ae846ef95c350b57483b8/transitflow/assets/clock_icon.png -------------------------------------------------------------------------------- /transitflow/concatenate_csvs.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import glob 3 | import os 4 | import argparse 5 | import math 6 | import datetime as dt 7 | import numpy as np 8 | import shutil 9 | from string import Template 10 | 11 | def concatenate_csvs(path): 12 | all_files = glob.glob(os.path.join(path, "*.csv")) # advisable to use os.path.join as this makes concatenation OS independent 13 | df_from_each_file = (pd.read_csv(f) for f in all_files) # generators 14 | concatenated_df = pd.concat(df_from_each_file, ignore_index=True) 15 | del concatenated_df['Unnamed: 0'] # delete the blank column that gets added 16 | concatenated_df['start_time'] = pd.to_datetime(concatenated_df['start_time']) 17 | concatenated_df['end_time'] = pd.to_datetime(concatenated_df['end_time']) 18 | concatenated_df = concatenated_df.sort_values(by="start_time").reset_index(drop=True) 19 | return concatenated_df 20 | 21 | # Calculate bearing 22 | # See: https://gis.stackexchange.com/questions/29239/calculate-bearing-between-two-decimal-gps-coordinates/48911 23 | def calc_bearing_between_points(startLat, startLong, endLat, endLong): 24 | 25 | startLat = math.radians(startLat) 26 | startLong = math.radians(startLong) 27 | endLat = math.radians(endLat) 28 | endLong = math.radians(endLong) 29 | dLong = endLong - startLong 30 | dPhi = math.log(math.tan(endLat/2.0+math.pi/4.0)/math.tan(startLat/2.0+math.pi/4.0)) 31 | if abs(dLong) > math.pi: 32 | if dLong > 0.0: 33 | dLong = -(2.0 * math.pi - dLong) 34 | else: 35 | dLong = (2.0 * math.pi + dLong) 36 | bearing = (math.degrees(math.atan2(dLong, dPhi)) + 360.0) % 360.0; 37 | return bearing 38 | 39 | # Stacked bar chart functions 40 | def count_vehicles_on_screen(concatenated_df, date, frames): 41 | number_of_vehicles = [] 42 | number_of_buses = [] 43 | number_of_trams = [] 44 | number_of_cablecars = [] 45 | number_of_metros = [] 46 | number_of_trains = [] 47 | number_of_ferries = [] 48 | 49 | day = dt.datetime.strptime(date, "%Y-%m-%d") 50 | thisday = dt.datetime.strftime(day, "%Y-%m-%d") 51 | 52 | chunks = float(frames) / (60*24) 53 | increment = float(60.0 / chunks) 54 | 55 | the_day = [pd.to_datetime(thisday) + dt.timedelta(seconds = i*increment) for i in range(int(60 * 24 * chunks))] 56 | 57 | count = 0 58 | for increment in the_day: 59 | 60 | vehicles_on_the_road = concatenated_df[(concatenated_df['end_time'] > increment) & (concatenated_df['start_time'] <= increment)] 61 | number_vehicles_on_the_road = len(vehicles_on_the_road) 62 | number_of_vehicles.append(number_vehicles_on_the_road) 63 | 64 | for route_type in ['bus', 'tram', 'cablecar', 'metro', 'rail', 'ferry']: 65 | just_this_mode = vehicles_on_the_road[vehicles_on_the_road['route_type'] == route_type] 66 | number_of_this_mode = len(just_this_mode) 67 | if route_type == 'bus': 68 | number_of_buses.append(number_of_this_mode) 69 | elif route_type == 'tram': 70 | number_of_trams.append(number_of_this_mode) 71 | elif route_type == 'cablecar': 72 | number_of_cablecars.append(number_of_this_mode) 73 | elif route_type == 'metro': 74 | number_of_metros.append(number_of_this_mode) 75 | elif route_type == 'rail': 76 | number_of_trains.append(number_of_this_mode) 77 | elif route_type == 'ferry': 78 | number_of_ferries.append(number_of_this_mode) 79 | 80 | if count % (60*chunks) == 0: 81 | print increment 82 | 83 | count += 1 84 | 85 | vehicles = pd.DataFrame(zip(the_day, number_of_vehicles)) 86 | print len(vehicles.index), "= length of vehicles index" 87 | buses = pd.DataFrame(zip(the_day, number_of_buses)) 88 | trams = pd.DataFrame(zip(the_day, number_of_trams)) 89 | cablecars = pd.DataFrame(zip(the_day, number_of_cablecars)) 90 | metros = pd.DataFrame(zip(the_day, number_of_metros)) 91 | trains = pd.DataFrame(zip(the_day, number_of_trains)) 92 | ferries = pd.DataFrame(zip(the_day, number_of_ferries)) 93 | 94 | for df in [vehicles, buses, trams, metros, cablecars, trains, ferries]: 95 | df.columns = ['time', 'count'] 96 | 97 | return vehicles, buses, trams, metros, cablecars, trains, ferries 98 | 99 | if __name__ == "__main__": 100 | parser = argparse.ArgumentParser() 101 | parser.add_argument("--date", help="Animation day") 102 | parser.add_argument("--apikey", help="Mapzen API Key") 103 | parser.add_argument( 104 | "--name", 105 | help="Output directory name", 106 | default="output" 107 | ) 108 | parser.add_argument( 109 | "--bbox", 110 | help="Bounding box" 111 | ) 112 | parser.add_argument( 113 | "--clip_to_bbox", 114 | help="Clip trips to bounding box", 115 | action="store_true" 116 | ) 117 | parser.add_argument( 118 | "--frames", 119 | help="Number of frames in animation. 3600 frames = 60 second animation.", 120 | default=3600 121 | ) 122 | parser.add_argument( 123 | "--animate", 124 | help="Generate processing sketch file.", 125 | action="store_true" 126 | ) 127 | parser.add_argument( 128 | "--recording", 129 | help="Records sketch to mp4", 130 | action="store_true" 131 | ) 132 | parser.add_argument( 133 | "--skip_bearings", 134 | help="Skip the calculate bearings between points step when concatenating csvs.", 135 | action="store_true" 136 | ) 137 | args = parser.parse_args() 138 | 139 | if not args.date: 140 | raise Exception('date required') 141 | 142 | MAPZEN_APIKEY = args.apikey 143 | OUTPUT_NAME = args.name 144 | DATE = args.date 145 | west, south, east, north = 0, 0, 0, 0 #null island! 146 | FRAMES = args.frames 147 | RECORDING = args.recording 148 | 149 | #print "" 150 | print("INPUTS:") 151 | print("date: ", DATE) 152 | print("name: ", OUTPUT_NAME) 153 | print("API key: ", MAPZEN_APIKEY) 154 | 155 | if args.bbox: 156 | west, south, east, north = args.bbox.split(",") 157 | # west, south, east, north = args.bbox.split(",") 158 | # bbox = true 159 | 160 | df = concatenate_csvs("sketches/{}/{}/data/indiv_operators".format(OUTPUT_NAME, DATE)) 161 | 162 | if not args.skip_bearings: 163 | print("Calculating trip segment bearings.") 164 | df['bearing'] = df.apply(lambda row: calc_bearing_between_points(row['start_lat'], row['start_lon'], row['end_lat'], row['end_lon']), axis=1) 165 | 166 | if args.bbox and args.clip_to_bbox: 167 | df = df[ 168 | ((df['start_lat'] >= float(south)) & (df['start_lat'] <= float(north)) & (df['start_lon'] >= float(west)) & (df['start_lon'] <= float(east))) & 169 | ((df['end_lat'] >= float(south)) & (df['end_lat'] <= float(north)) & (df['end_lon'] >= float(west)) & (df['end_lon'] <= float(east))) 170 | ] 171 | 172 | # Save to csv. 173 | df.to_csv("sketches/{}/{}/data/output.csv".format(OUTPUT_NAME, DATE)) 174 | print("Total rows: ", df.shape[0]) 175 | 176 | print("Counting number of vehicles in transit.") 177 | vehicles, buses, trams, metros, cablecars, trains, ferries = count_vehicles_on_screen(df, DATE, FRAMES) 178 | print("Frames: ", FRAMES) 179 | 180 | # ### Save vehicle counts to csv (3600 frame version) 181 | # Our Processing sketch has 3,600 frames (at 60 frames per second makes 182 | # a one minute video). One day has 5,760 15-second intervals. So to make 183 | # things easy we will select the vehicle counts at 3,600 of the 15-second 184 | # intervals throughout the day. We will select them randomly, but will 185 | # maintain chronological order by sorting and also consistency between 186 | # vehicle types by using a consitent set of random indices to select 187 | # counts for different vehicle types. 188 | 189 | random_indices = np.sort(np.random.choice(vehicles.index, int(FRAMES), replace=False)) 190 | 191 | vehicles_counts_output = vehicles.loc[random_indices].reset_index(drop=True) 192 | vehicles_counts_output['frame'] = vehicles_counts_output.index 193 | buses_counts_output = buses.loc[random_indices].reset_index(drop=True) 194 | buses_counts_output['frame'] = buses_counts_output.index 195 | trams_counts_output = trams.loc[random_indices].reset_index(drop=True) 196 | trams_counts_output['frame'] = trams_counts_output.index 197 | metros_counts_output = metros.loc[random_indices].reset_index(drop=True) 198 | metros_counts_output['frame'] = metros_counts_output.index 199 | cablecars_counts_output = cablecars.loc[random_indices].reset_index(drop=True) 200 | cablecars_counts_output['frame'] = cablecars_counts_output.index 201 | trains_counts_output = trains.loc[random_indices].reset_index(drop=True) 202 | trains_counts_output['frame'] = trains_counts_output.index 203 | ferries_counts_output = ferries.loc[random_indices].reset_index(drop=True) 204 | ferries_counts_output['frame'] = ferries_counts_output.index 205 | 206 | # Save these vehicle count stats to csv's. 207 | if not os.path.exists("sketches/{}/{}/data/vehicle_counts".format(OUTPUT_NAME, DATE)): 208 | os.makedirs("sketches/{}/{}/data/vehicle_counts".format(OUTPUT_NAME, DATE)) 209 | vehicles_counts_output.to_csv("sketches/{}/{}/data/vehicle_counts/vehicles_{}.csv".format(OUTPUT_NAME, DATE, FRAMES)) 210 | buses_counts_output.to_csv("sketches/{}/{}/data/vehicle_counts/buses_{}.csv".format(OUTPUT_NAME, DATE, FRAMES)) 211 | trams_counts_output.to_csv("sketches/{}/{}/data/vehicle_counts/trams_{}.csv".format(OUTPUT_NAME, DATE, FRAMES)) 212 | metros_counts_output.to_csv("sketches/{}/{}/data/vehicle_counts/metros_{}.csv".format(OUTPUT_NAME, DATE, FRAMES)) 213 | cablecars_counts_output.to_csv("sketches/{}/{}/data/vehicle_counts/cablecars_{}.csv".format(OUTPUT_NAME, DATE, FRAMES)) 214 | trains_counts_output.to_csv("sketches/{}/{}/data/vehicle_counts/trains_{}.csv".format(OUTPUT_NAME, DATE, FRAMES)) 215 | ferries_counts_output.to_csv("sketches/{}/{}/data/vehicle_counts/ferries_{}.csv".format(OUTPUT_NAME, DATE, FRAMES)) 216 | 217 | # Hacky way to center the sketch 218 | if not args.bbox: 219 | south, west, north, east = df['start_lat'][0], df['start_lon'][0], df['start_lat'][1], df['start_lon'][1] 220 | 221 | ## Use processing sketch template to create processing sketch file 222 | if args.animate: 223 | module_path = os.path.join(os.path.dirname(__file__)) 224 | template_path = os.path.join(module_path, 'templates', 'template.pde') 225 | with open(template_path) as f: 226 | data = f.read() 227 | s = Template(data) 228 | 229 | if not os.path.exists("sketches/{}/{}/sketch".format(OUTPUT_NAME, DATE)): 230 | os.makedirs("sketches/{}/{}/sketch".format(OUTPUT_NAME, DATE)) 231 | 232 | for asset in ['calendar_icon.png', 'clock_icon.png']: 233 | shutil.copyfile( 234 | os.path.join(module_path, 'assets', asset), 235 | os.path.join('sketches', OUTPUT_NAME, DATE, "sketch", asset) 236 | ) 237 | 238 | with open("sketches/{}/{}/sketch/sketch.pde".format(OUTPUT_NAME, DATE), "w") as f: 239 | f.write( 240 | s.substitute( 241 | DIRECTORY_NAME=OUTPUT_NAME, 242 | DATE=DATE, 243 | TOTAL_FRAMES=FRAMES, 244 | RECORDING=str(RECORDING).lower(), 245 | AVG_LAT=(float(south) + float(north))/2.0, 246 | AVG_LON=(float(west) + float(east))/2.0 247 | ) 248 | ) 249 | -------------------------------------------------------------------------------- /transitflow/templates/template.pde: -------------------------------------------------------------------------------- 1 | /* 2 | TransitFlow 3 | https://github.com/transitland/transitland-processing-animation 4 | Will Geary (@wgeary) for Mapzen, 2017 5 | 6 | Attribution: 7 | Juan Francisco Saldarriaga's workshop on Processing: https://github.com/juanfrans-courses/DataScienceSocietyWorkshop 8 | Till Nagel's Unfolding Maps library: http://unfoldingmaps.org/ 9 | */ 10 | 11 | ////// MAIN INPUTS /////// 12 | String directoryName = "${DIRECTORY_NAME}"; 13 | String date = "${DATE}"; 14 | String inputFile = "../data/output.csv"; 15 | int totalFrames = ${TOTAL_FRAMES}; 16 | Location center = new Location(${AVG_LAT}, ${AVG_LON}); 17 | Integer zoom_start = 9; 18 | String date_format = "M/d/yy"; 19 | String day_format = "EEEE"; 20 | String time_format = "h:mm a"; 21 | boolean recording = ${RECORDING}; 22 | boolean HQ = false; 23 | boolean rotateBearing = true; 24 | ////////////////////////// 25 | 26 | // Import Unfolding Maps 27 | import de.fhpotsdam.unfolding.*; 28 | import de.fhpotsdam.unfolding.core.*; 29 | import de.fhpotsdam.unfolding.data.*; 30 | import de.fhpotsdam.unfolding.events.*; 31 | import de.fhpotsdam.unfolding.geo.*; 32 | import de.fhpotsdam.unfolding.interactions.*; 33 | import de.fhpotsdam.unfolding.mapdisplay.*; 34 | import de.fhpotsdam.unfolding.mapdisplay.shaders.*; 35 | import de.fhpotsdam.unfolding.marker.*; 36 | import de.fhpotsdam.unfolding.providers.*; 37 | import de.fhpotsdam.unfolding.texture.*; 38 | import de.fhpotsdam.unfolding.tiles.*; 39 | import de.fhpotsdam.unfolding.ui.*; 40 | import de.fhpotsdam.unfolding.utils.*; 41 | import de.fhpotsdam.utils.*; 42 | 43 | // Import some Java utilities 44 | import java.util.Date; 45 | import java.text.SimpleDateFormat; 46 | 47 | // Import video export 48 | import com.hamoid.*; 49 | VideoExport videoExport; 50 | 51 | // Declare Global Variables 52 | 53 | UnfoldingMap map; 54 | 55 | // Statistics input files (for stacked area chart) 56 | String vehicleCountFile = "../data/vehicle_counts/vehicles_" + totalFrames + ".csv"; 57 | String busCountFile = "../data/vehicle_counts/buses_" + totalFrames + ".csv"; 58 | String tramCountFile = "../data/vehicle_counts/trams_" + totalFrames + ".csv"; 59 | String cablecarCountFile = "../data/vehicle_counts/cablecars_" + totalFrames + ".csv"; 60 | String metroCountFile = "../data/vehicle_counts/metros_" + totalFrames + ".csv"; 61 | String trainCountFile = "../data/vehicle_counts/trains_" + totalFrames + ".csv"; 62 | String ferryCountFile = "../data/vehicle_counts/ferries_" + totalFrames + ".csv"; 63 | 64 | int totalSeconds; 65 | Table tripTable; 66 | 67 | ArrayList trips = new ArrayList(); 68 | ArrayList operators = new ArrayList(); 69 | ArrayList vehicle_types = new ArrayList(); 70 | ArrayList bearings = new ArrayList(); 71 | 72 | Table vehicleCount; 73 | IntList vehicleCounts; 74 | int maxVehicleCount; 75 | float hscale = float(totalFrames) / float(width)*0.12; 76 | 77 | Table busCount; 78 | ArrayList busLines = new ArrayList(); 79 | ArrayList busFrames = new ArrayList(); 80 | ArrayList busCounts = new ArrayList(); 81 | ArrayList busHeights = new ArrayList(); 82 | 83 | Table tramCount; 84 | ArrayList tramLines = new ArrayList(); 85 | ArrayList tramFrames = new ArrayList(); 86 | ArrayList tramCounts = new ArrayList(); 87 | ArrayList tramHeights = new ArrayList(); 88 | 89 | Table metroCount; 90 | ArrayList metroLines = new ArrayList(); 91 | ArrayList metroFrames = new ArrayList(); 92 | ArrayList metroCounts = new ArrayList(); 93 | ArrayList metroHeights = new ArrayList(); 94 | 95 | Table cablecarCount; 96 | ArrayList cablecarLines = new ArrayList(); 97 | ArrayList cablecarFrames = new ArrayList(); 98 | ArrayList cablecarCounts = new ArrayList(); 99 | ArrayList cablecarHeights = new ArrayList(); 100 | 101 | Table trainCount; 102 | ArrayList trainLines = new ArrayList(); 103 | ArrayList trainFrames = new ArrayList(); 104 | ArrayList trainCounts = new ArrayList(); 105 | ArrayList trainHeights = new ArrayList(); 106 | 107 | Table ferryCount; 108 | ArrayList ferryLines = new ArrayList(); 109 | ArrayList ferryFrames = new ArrayList(); 110 | ArrayList ferryCounts = new ArrayList(); 111 | ArrayList ferryHeights = new ArrayList(); 112 | 113 | boolean bus_label = false; 114 | boolean tram_label = false; 115 | boolean metro_label = false; 116 | boolean train_label = false; 117 | boolean cablecar_label = false; 118 | boolean ferry_label = false; 119 | 120 | ScreenPosition startPos; 121 | ScreenPosition endPos; 122 | Location startLocation; 123 | Location endLocation; 124 | Date minDate; 125 | Date maxDate; 126 | Date startDate; 127 | Date endDate; 128 | Date thisStartDate; 129 | Date thisEndDate; 130 | PImage clock; 131 | PImage calendar; 132 | PImage airport; 133 | PFont raleway; 134 | PFont ralewayBold; 135 | Integer screenfillalpha = 120; 136 | 137 | 138 | Float firstLat; 139 | Float firstLon; 140 | color c; 141 | 142 | // define date format of raw data 143 | SimpleDateFormat myDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 144 | SimpleDateFormat hour = new SimpleDateFormat("h:mm a"); 145 | //SimpleDateFormat day = new SimpleDateFormat("MMMM dd, yyyy"); 146 | SimpleDateFormat weekday = new SimpleDateFormat("EEEE"); 147 | 148 | // Basemap providers 149 | AbstractMapProvider provider1; 150 | AbstractMapProvider provider2; 151 | AbstractMapProvider provider3; 152 | AbstractMapProvider provider4; 153 | AbstractMapProvider provider5; 154 | AbstractMapProvider provider6; 155 | AbstractMapProvider provider7; 156 | AbstractMapProvider provider8; 157 | AbstractMapProvider provider9; 158 | AbstractMapProvider provider0; 159 | AbstractMapProvider providerq; 160 | AbstractMapProvider providerw; 161 | AbstractMapProvider providere; 162 | AbstractMapProvider providerr; 163 | AbstractMapProvider providert; 164 | AbstractMapProvider providery; 165 | AbstractMapProvider provideru; 166 | AbstractMapProvider provideri; 167 | 168 | String provider1Attrib; 169 | String provider2Attrib; 170 | String provider3Attrib; 171 | String provider4Attrib; 172 | String provider5Attrib; 173 | String provider6Attrib; 174 | String provider7Attrib; 175 | String provider8Attrib; 176 | String provider9Attrib; 177 | String provider0Attrib; 178 | String providerqAttrib; 179 | String providerwAttrib; 180 | String providereAttrib; 181 | String providerrAttrib; 182 | String providertAttrib; 183 | String provideryAttrib; 184 | String provideruAttrib; 185 | 186 | boolean monday = false; 187 | boolean tuesday = false; 188 | boolean wednesday = false; 189 | boolean thursday = false; 190 | boolean friday = false; 191 | boolean saturday = false; 192 | boolean sunday = false; 193 | 194 | String attrib; 195 | Float attribWidth; 196 | 197 | void setup() { 198 | size(1000, 800, P3D); 199 | //size(1000, 960, P3D); 200 | //fullScreen(P3D); 201 | 202 | provider1 = new StamenMapProvider.TonerLite(); 203 | provider2 = new StamenMapProvider.TonerBackground(); 204 | provider3 = new CartoDB.Positron(); 205 | provider4 = new Microsoft.AerialProvider(); 206 | provider5 = new OpenStreetMap.OpenStreetMapProvider(); 207 | provider6 = new OpenStreetMap.OSMGrayProvider(); 208 | provider7 = new EsriProvider.WorldStreetMap(); 209 | provider8 = new EsriProvider.DeLorme(); 210 | provider9 = new EsriProvider.WorldShadedRelief(); 211 | provider0 = new EsriProvider.NatGeoWorldMap(); 212 | providerq = new EsriProvider.OceanBasemap(); 213 | providerw = new EsriProvider.WorldGrayCanvas(); 214 | providere = new EsriProvider.WorldPhysical(); 215 | providerr = new EsriProvider.WorldStreetMap(); 216 | providert = new EsriProvider.WorldTerrain(); 217 | providery = new EsriProvider.WorldTopoMap(); 218 | provideru = new Google.GoogleMapProvider(); 219 | 220 | provider1Attrib = "Stamen Design"; 221 | provider2Attrib = "Stamen Design"; 222 | provider3Attrib = "Carto"; 223 | provider4Attrib = "Bing Maps"; 224 | provider5Attrib = "OpenStreetMap"; 225 | provider6Attrib = "OpenStreetMap"; 226 | provider7Attrib = "ESRI"; 227 | provider8Attrib = "ESRI"; 228 | provider9Attrib = "ESRI"; 229 | provider0Attrib = "ESRI"; 230 | providerqAttrib = "ESRI"; 231 | providerwAttrib = "ESRI"; 232 | providereAttrib = "ESRI"; 233 | providerrAttrib = "ESRI"; 234 | providertAttrib = "ESRI"; 235 | provideryAttrib = "ESRI"; 236 | provideruAttrib = "Google Maps"; 237 | 238 | smooth(); 239 | 240 | loadData(); 241 | 242 | map = new UnfoldingMap(this, provider1); 243 | MapUtils.createDefaultEventDispatcher(this, map); 244 | 245 | attrib = "© Mapzen | Transitland | Unfolding Maps | Basemap by " + provider1Attrib; 246 | attribWidth = textWidth(attrib); 247 | 248 | map.zoomAndPanTo(zoom_start, center); 249 | 250 | // Fonts and icons 251 | raleway = createFont("Raleway-Heavy", 32); 252 | ralewayBold = createFont("Raleway-Bold", 28); 253 | clock = loadImage("clock_icon.png"); 254 | clock.resize(0, 35); 255 | calendar = loadImage("calendar_icon.png"); 256 | calendar.resize(0, 35); 257 | 258 | videoExport = new VideoExport(this); 259 | videoExport.setFrameRate(60); 260 | if (recording == true) videoExport.startMovie(); 261 | } 262 | 263 | float h_offset; 264 | 265 | void loadData() { 266 | tripTable = loadTable(inputFile, "header"); 267 | println(str(tripTable.getRowCount()) + " records loaded..."); 268 | 269 | // calculate min start time and max end time (must be sorted ascending) 270 | String first = tripTable.getString(0, "start_time"); 271 | String last = tripTable.getString(tripTable.getRowCount()-1, "end_time"); 272 | 273 | println("Min departure time: ", first); 274 | println("Max departure time: ", last); 275 | 276 | try { 277 | minDate = myDateFormat.parse(first); //first "2017-07-17 9:59:00" 278 | maxDate = myDateFormat.parse(last); //last 279 | totalSeconds = int(maxDate.getTime()/1000) - int(minDate.getTime()/1000); 280 | } 281 | catch (Exception e) { 282 | println("Unable to parse date stamp"); 283 | } 284 | println("Min starttime:", minDate, ". In epoch:", minDate.getTime()/1000); 285 | println("Max starttime:", maxDate, ". In epoch:", maxDate.getTime()/1000); 286 | println("Total seconds in dataset:", totalSeconds); 287 | println("Total frames:", totalFrames); 288 | 289 | firstLat = tripTable.getFloat(0, "start_lat"); 290 | firstLon = tripTable.getFloat(0, "start_lon"); 291 | 292 | for (TableRow row : tripTable.rows()) { 293 | String vehicle_type = row.getString("route_type"); 294 | vehicle_types.add(vehicle_type); 295 | 296 | Float bearing = row.getFloat("bearing"); 297 | bearings.add(bearing); 298 | 299 | int tripduration = row.getInt("duration"); 300 | int duration = round(map(tripduration, 0, totalSeconds, 0, totalFrames)); 301 | 302 | try { 303 | thisStartDate = myDateFormat.parse(row.getString("start_time")); 304 | thisEndDate = myDateFormat.parse(row.getString("end_time")); 305 | } 306 | catch (Exception e) { 307 | println("Unable to parse destination"); 308 | } 309 | 310 | int startFrame = floor(map(thisStartDate.getTime()/1000, minDate.getTime()/1000, maxDate.getTime()/1000, 0, totalFrames)); 311 | int endFrame = floor(map(thisEndDate.getTime()/1000, minDate.getTime()/1000, maxDate.getTime()/1000, 0, totalFrames)); 312 | 313 | float startLat = row.getFloat("start_lat"); 314 | float startLon = row.getFloat("start_lon"); 315 | float endLat = row.getFloat("end_lat"); 316 | float endLon = row.getFloat("end_lon"); 317 | startLocation = new Location(startLat, startLon); 318 | endLocation = new Location(endLat, endLon); 319 | trips.add(new Trips(duration, startFrame, endFrame, startLocation, endLocation, bearing)); 320 | } 321 | 322 | 323 | int lineAlpha = 80; 324 | // total vehicle counts 325 | vehicleCount = loadTable(vehicleCountFile, "header"); 326 | vehicleCounts = new IntList(); 327 | for (int i = 0; i < vehicleCount.getRowCount(); i++) { 328 | TableRow row = vehicleCount.getRow(i); 329 | int count = row.getInt("count"); 330 | vehicleCounts.append(count); 331 | } 332 | maxVehicleCount = int(vehicleCounts.max()); 333 | println("Max vehicles on road: " + maxVehicleCount); 334 | 335 | // maximum height of stacked bar chart in pixels 336 | int maxPixels = 140; 337 | 338 | // bus stacked bar 339 | busCount = loadTable(busCountFile, "header"); 340 | for (int i = 0; i < busCount.getRowCount(); i++) { 341 | TableRow row = busCount.getRow(i); 342 | int frame = row.getInt("frame"); 343 | int count = row.getInt("count"); 344 | busFrames.add(frame); 345 | busCounts.add(count); 346 | float h = busCounts.get(i); 347 | c = color(0, 173, 253, lineAlpha); 348 | int xmargin = 50; 349 | int ymargin = 40; 350 | int xaxisoffset = 5; 351 | float lineHeight = map(h, 0, maxVehicleCount, 0, maxPixels); 352 | busHeights.add(lineHeight); 353 | Line line = new Line(xmargin + i/hscale, height-ymargin, xmargin + i/hscale, height-ymargin-lineHeight, c); 354 | busLines.add(line); 355 | } 356 | 357 | // tram stacked bar 358 | tramCount = loadTable(tramCountFile, "header"); 359 | for (int i = 0; i < tramCount.getRowCount(); i++) { 360 | TableRow row = tramCount.getRow(i); 361 | int frame = row.getInt("frame"); 362 | int count = row.getInt("count"); 363 | tramFrames.add(frame); 364 | tramCounts.add(count); 365 | float h = tramCounts.get(i); 366 | float lineHeight = map(h, 0, maxVehicleCount, 0, maxPixels); 367 | tramHeights.add(lineHeight); 368 | h_offset = busHeights.get(i); 369 | c = color(124, 252, 0, lineAlpha); 370 | int xmargin = 50; 371 | int ymargin = 40; 372 | Line line = new Line(xmargin + i/hscale, height-ymargin-(h_offset), xmargin + i/hscale, height-ymargin-(lineHeight+h_offset), c); 373 | tramLines.add(line); 374 | } 375 | 376 | // Metro stacked bar 377 | metroCount = loadTable(metroCountFile, "header"); 378 | for (int i = 0; i < metroCount.getRowCount(); i++) { 379 | TableRow row = metroCount.getRow(i); 380 | int frame = row.getInt("frame"); 381 | int count = row.getInt("count"); 382 | metroFrames.add(frame); 383 | metroCounts.add(count); 384 | float h = metroCounts.get(i); 385 | float lineHeight = map(h, 0, maxVehicleCount, 0, maxPixels); 386 | metroHeights.add(lineHeight); 387 | h_offset = busHeights.get(i) + tramHeights.get(i); 388 | c = color(255, 0, 0, lineAlpha); 389 | int xmargin = 50; 390 | int ymargin = 40; 391 | Line line = new Line(xmargin + i/hscale, height-ymargin-(h_offset), xmargin + i/hscale, height-ymargin-(lineHeight+h_offset), c); 392 | metroLines.add(line); 393 | } 394 | 395 | // Train stacked bar 396 | trainCount = loadTable(trainCountFile, "header"); 397 | for (int i = 0; i < trainCount.getRowCount(); i++) { 398 | TableRow row = trainCount.getRow(i); 399 | int frame = row.getInt("frame"); 400 | int count = row.getInt("count"); 401 | trainFrames.add(frame); 402 | trainCounts.add(count); 403 | float h = trainCounts.get(i); 404 | float lineHeight = map(h, 0, maxVehicleCount, 0, maxPixels); 405 | trainHeights.add(lineHeight); 406 | h_offset = busHeights.get(i)+ tramHeights.get(i)+ metroHeights.get(i); 407 | c = color(255, 215, 0, lineAlpha); 408 | int xmargin = 50; 409 | int ymargin = 40; 410 | Line line = new Line(xmargin + i/hscale, height-ymargin-(h_offset), xmargin + i/hscale, height-ymargin-(lineHeight+h_offset), c); 411 | trainLines.add(line); 412 | } 413 | 414 | // Cablecar stacked bar 415 | cablecarCount = loadTable(cablecarCountFile, "header"); 416 | for (int i = 0; i < cablecarCount.getRowCount(); i++) { 417 | TableRow row = cablecarCount.getRow(i); 418 | int frame = row.getInt("frame"); 419 | int count = row.getInt("count"); 420 | cablecarFrames.add(frame); 421 | cablecarCounts.add(count); 422 | float h = cablecarCounts.get(i); 423 | float lineHeight = map(h, 0, maxVehicleCount, 0, maxPixels); 424 | cablecarHeights.add(lineHeight); 425 | h_offset = busHeights.get(i) + tramHeights.get(i) + metroHeights.get(i) + trainHeights.get(i); 426 | c = color(255,140,0, lineAlpha); 427 | int xmargin = 50; 428 | int ymargin = 40; 429 | Line line = new Line(xmargin + i/hscale, height-ymargin-(h_offset), xmargin + i/hscale, height-ymargin-(lineHeight+h_offset), c); 430 | cablecarLines.add(line); 431 | } 432 | 433 | // Ferry stacked bar 434 | ferryCount = loadTable(ferryCountFile, "header"); 435 | for (int i = 0; i < ferryCount.getRowCount(); i++) { 436 | TableRow row = ferryCount.getRow(i); 437 | int frame = row.getInt("frame"); 438 | int count = row.getInt("count"); 439 | ferryFrames.add(frame); 440 | ferryCounts.add(count); 441 | float h = ferryCounts.get(i); 442 | float lineHeight = map(h, 0, maxVehicleCount, 0, maxPixels); 443 | ferryHeights.add(lineHeight); 444 | h_offset = busHeights.get(i) + tramHeights.get(i) + metroHeights.get(i) + cablecarHeights.get(i) + trainHeights.get(i); 445 | c = color(255, 105, 180, lineAlpha); 446 | int xmargin = 50; 447 | int ymargin = 40; 448 | Line line = new Line(xmargin + i/hscale, height-ymargin-(h_offset), xmargin + i/hscale, height-ymargin-(lineHeight+h_offset), c); 449 | ferryLines.add(line); 450 | } 451 | } 452 | 453 | void draw() { 454 | 455 | if (frameCount < totalFrames) { 456 | map.draw(); 457 | noStroke(); 458 | fill(0, screenfillalpha); 459 | rect(0, 0, width, height); 460 | 461 | // draw stacked bar chart with a bunch of skinny lines 462 | for (int i = 0; i < frameCount; i++) { 463 | Line busLine = busLines.get(i); 464 | busLine.plot(); 465 | 466 | Line trainLine = trainLines.get(i); 467 | trainLine.plot(); 468 | 469 | Line tramLine = tramLines.get(i); 470 | tramLine.plot(); 471 | 472 | Line metroLine = metroLines.get(i); 473 | metroLine.plot(); 474 | 475 | Line cablecarLine = cablecarLines.get(i); 476 | cablecarLine.plot(); 477 | 478 | Line ferryLine = ferryLines.get(i); 479 | ferryLine.plot(); 480 | } 481 | 482 | // handle time 483 | float epoch_float = map(frameCount, 0, totalFrames, int(minDate.getTime()/1000), int(maxDate.getTime()/1000)); 484 | int epoch = int(epoch_float); 485 | 486 | String date = new java.text.SimpleDateFormat(date_format).format(new java.util.Date(epoch * 1000L)); 487 | String day = new java.text.SimpleDateFormat(day_format).format(new java.util.Date(epoch * 1000L)); 488 | String time = new java.text.SimpleDateFormat(time_format).format(new java.util.Date(epoch * 1000L)); 489 | 490 | // draw labels 491 | textFont(ralewayBold, 12); 492 | int xmargin = 60; 493 | int ymargin = 40; 494 | 495 | //fill(255, 255, 255); 496 | 497 | int h_bus = busCounts.get(frameCount); 498 | int h_tram = tramCounts.get(frameCount); 499 | int h_metro = metroCounts.get(frameCount); 500 | int h_train = trainCounts.get(frameCount); 501 | int h_cablecar = cablecarCounts.get(frameCount); 502 | int h_ferry = ferryCounts.get(frameCount); 503 | 504 | int ymargin_adjust = 30; 505 | 506 | if (h_bus != 0 || bus_label == true) ymargin_adjust = ymargin_adjust + 15; 507 | if (h_tram != 0 || tram_label == true) ymargin_adjust = ymargin_adjust + 15; 508 | if (h_metro != 0 || metro_label == true) ymargin_adjust = ymargin_adjust + 15; 509 | if (h_train != 0 || train_label == true) ymargin_adjust = ymargin_adjust + 15; 510 | if (h_cablecar != 0 || cablecar_label == true) ymargin_adjust = ymargin_adjust + 15; 511 | if (h_ferry != 0 || ferry_label == true) ymargin_adjust = ymargin_adjust + 15; 512 | 513 | fill(0,150); 514 | noStroke(); 515 | rect(xmargin + frameCount/hscale-5, height-ymargin - ymargin_adjust, 110, ymargin_adjust + 15, 7); 516 | fill(150, 220); 517 | rect(width-(attribWidth+10), height-18, (attribWidth+10), 18, 3); 518 | c = color(255,255,255,255); 519 | strokeWeight(1); 520 | stroke(c); 521 | line(xmargin + frameCount/hscale, height-ymargin-24, xmargin + frameCount/hscale+100, height-ymargin-24); 522 | 523 | noStroke(); 524 | 525 | ymargin_adjust = ymargin_adjust - 15; 526 | 527 | if (h_bus != 0 || bus_label == true) { 528 | fill(0, 173, 253, 255); 529 | text("Buses: ", xmargin + frameCount/hscale, height - ymargin - ymargin_adjust); 530 | textAlign(RIGHT); 531 | text(nfc(h_bus), xmargin + frameCount/hscale + 100, height - ymargin - ymargin_adjust); 532 | textAlign(LEFT); 533 | ymargin_adjust = ymargin_adjust - 15; 534 | bus_label = true; 535 | } 536 | 537 | if (h_tram != 0 || tram_label == true) { 538 | fill(124, 252, 0, 255); 539 | text("Light Rail: ", xmargin + frameCount/hscale, height-ymargin - ymargin_adjust); 540 | textAlign(RIGHT); 541 | text(h_tram, xmargin + frameCount/hscale + 100, height-ymargin - ymargin_adjust); 542 | textAlign(LEFT); 543 | ymargin_adjust = ymargin_adjust - 15; 544 | tram_label = true; 545 | } 546 | 547 | if (h_metro != 0 || metro_label == true) { 548 | fill(255,51,51, 255); 549 | text("Subways: ", xmargin + frameCount/hscale, height-ymargin - ymargin_adjust); 550 | textAlign(RIGHT); 551 | text(h_metro, xmargin + frameCount/hscale + 100, height-ymargin - ymargin_adjust); 552 | textAlign(LEFT); 553 | ymargin_adjust = ymargin_adjust - 15; 554 | metro_label = true; 555 | } 556 | 557 | if (h_train != 0 || train_label == true) { 558 | fill(255, 215, 0, 255); 559 | text("Trains: ", xmargin + frameCount/hscale, height-ymargin - ymargin_adjust); 560 | textAlign(RIGHT); 561 | text(h_train, xmargin + frameCount/hscale + 100, height-ymargin - ymargin_adjust); 562 | textAlign(LEFT); 563 | ymargin_adjust = ymargin_adjust - 15; 564 | train_label = true; 565 | } 566 | 567 | if (h_cablecar != 0 || cablecar_label == true) { 568 | fill(255,140,0, 255); 569 | text("Cable Cars: ", xmargin + frameCount/hscale, height-ymargin - ymargin_adjust); 570 | textAlign(RIGHT); 571 | text(h_cablecar, xmargin + frameCount/hscale + 100, height-ymargin - ymargin_adjust); 572 | textAlign(LEFT); 573 | ymargin_adjust = ymargin_adjust - 15; 574 | cablecar_label = true; 575 | } 576 | 577 | if (h_ferry != 0 || ferry_label == true) { 578 | fill(255, 105, 180, 255); 579 | text("Ferries: ", xmargin + frameCount/hscale, height-ymargin - ymargin_adjust); 580 | textAlign(RIGHT); 581 | text(h_ferry, xmargin + frameCount/hscale + 100, height-ymargin - ymargin_adjust); 582 | textAlign(LEFT); 583 | ymargin_adjust = ymargin_adjust - 15; 584 | ferry_label = true; 585 | } 586 | 587 | fill(255); 588 | text("Total: ", xmargin + frameCount/hscale, height-ymargin-10); 589 | textAlign(RIGHT); 590 | text(nfc(h_bus + h_train + h_tram + h_metro + h_cablecar + h_ferry), xmargin + frameCount/hscale + 100, height-ymargin-10); 591 | textAlign(LEFT); 592 | 593 | if (day.equals("Monday")) monday = true; 594 | if (day.equals("Tuesday")) tuesday = true; 595 | if (day.equals("Wednesday")) wednesday = true; 596 | if (day.equals("Thursday")) thursday = true; 597 | if (day.equals("Friday")) friday = true; 598 | if (day.equals("Saturday")) saturday = true; 599 | if (day.equals("Sunday")) sunday = true; 600 | 601 | int xlabeloffset = 15; 602 | //int xlabeldist = 120; 603 | int xlabelmargin = 50; 604 | 605 | fill(255); 606 | if (monday == true) text("Monday", xlabelmargin, height-xlabeloffset); 607 | 608 | text(time, xmargin + frameCount/hscale, height - xlabeloffset-15); 609 | // X axis 610 | rect(xmargin-10, height-xlabeloffset-20, frameCount/hscale, 1); 611 | 612 | textFont(ralewayBold, 14); 613 | text("Number of Transit Vehicles in Motion", xlabelmargin, height-xlabeloffset-170); 614 | 615 | fill(0, screenfillalpha); 616 | 617 | // draw trips 618 | noStroke(); 619 | for (int i=0; i < trips.size(); i++) { 620 | 621 | Trips trip = trips.get(i); 622 | String vehicle_type = vehicle_types.get(i); 623 | 624 | switch(vehicle_type) { 625 | case "bus": 626 | c = color(0, 173, 253); 627 | fill(c, 240); 628 | trip.plotBus(); 629 | break; 630 | case "bus_service": 631 | c = color(0, 173, 253); 632 | fill(c, 240); 633 | trip.plotBus(); 634 | break; 635 | case("tram"): 636 | c = color(124, 252, 0); 637 | fill(c, 245); 638 | trip.plotTram(); 639 | break; 640 | case("metro"): 641 | c = color(255, 0, 0); 642 | fill(c, 245); 643 | trip.plotSubway(); 644 | break; 645 | case("rail"): 646 | c = color(255, 215, 0); 647 | fill(c, 200); 648 | trip.plotTrain(); 649 | break; 650 | case("ferry"): 651 | c = color(255, 105, 180); 652 | fill(c, 200); 653 | trip.plotFerry(); 654 | break; 655 | case("cablecar"): 656 | c = color(255,140,0); 657 | fill(c, 200); 658 | trip.plotRide(); 659 | case("gondola"): 660 | c = color(255, 127, 80); 661 | fill(c, 200); 662 | trip.plotRide(); 663 | case("funicular"): 664 | c = color(0, 173, 253); 665 | fill(c, 255); 666 | trip.plotRide(); 667 | } 668 | } 669 | // Time and icons 670 | textSize(32); 671 | fill(255, 255, 255, 255); 672 | image(clock, 30, 25); 673 | stroke(255, 255, 255, 255); 674 | line(30, 70, 210, 70); 675 | image(calendar, 30, 80 ); 676 | 677 | textFont(raleway); 678 | noStroke(); 679 | text(time, 75, 55); 680 | textFont(ralewayBold); 681 | text(day, 75, 107); 682 | 683 | textSize(16); 684 | text(date, 75, 128); 685 | 686 | textSize(12); 687 | text(attrib, width-(attribWidth+5), height-5); 688 | 689 | if (recording == true) { 690 | if (frameCount < totalFrames) { 691 | if (HQ == true) { 692 | saveFrame("frames/######.tiff"); 693 | } else if (HQ == false) { 694 | videoExport.saveFrame(); 695 | return; 696 | } 697 | } else { 698 | if (HQ == true) exit(); 699 | if (HQ == false) videoExport.endMovie(); 700 | exit(); 701 | } 702 | } 703 | } 704 | } 705 | 706 | void keyPressed() { 707 | if (key == '1') { 708 | map.mapDisplay.setProvider(provider1); 709 | attrib = "© Mapzen | Transitland | Unfolding Maps | " + provider1Attrib; 710 | attribWidth = textWidth(attrib); 711 | } else if (key == '2') { 712 | map.mapDisplay.setProvider(provider2); 713 | attrib = "© Mapzen | Transitland | Unfolding Maps | " + provider2Attrib; 714 | attribWidth = textWidth(attrib); 715 | } else if (key == '3') { 716 | map.mapDisplay.setProvider(provider3); 717 | attrib = "© Mapzen | Transitland | Unfolding Maps | " + provider3Attrib; 718 | attribWidth = textWidth(attrib); 719 | } else if (key == '4') { 720 | map.mapDisplay.setProvider(provider4); 721 | attrib = "© Mapzen | Transitland | Unfolding Maps | " + provider4Attrib; 722 | attribWidth = textWidth(attrib); 723 | } else if (key == '5') { 724 | map.mapDisplay.setProvider(provider5); 725 | attrib = "© Mapzen | Transitland | Unfolding Maps | " + provider5Attrib; 726 | attribWidth = textWidth(attrib); 727 | } else if (key == '6') { 728 | map.mapDisplay.setProvider(provider6); 729 | attrib = "© Mapzen | Transitland | Unfolding Maps | " + provider6Attrib; 730 | attribWidth = textWidth(attrib); 731 | } else if (key == '7') { 732 | map.mapDisplay.setProvider(provider7); 733 | attrib = "© Mapzen | Transitland | Unfolding Maps | " + provider7Attrib; 734 | attribWidth = textWidth(attrib); 735 | } else if (key == '8') { 736 | map.mapDisplay.setProvider(provider8); 737 | attrib = "© Mapzen | Transitland | Unfolding Maps | " + provider8Attrib; 738 | attribWidth = textWidth(attrib); 739 | } else if (key == '9') { 740 | map.mapDisplay.setProvider(provider9); 741 | attrib = "© Mapzen | Transitland | Unfolding Maps | " + provider9Attrib; 742 | attribWidth = textWidth(attrib); 743 | } else if (key == '0') { 744 | map.mapDisplay.setProvider(provider0); 745 | attrib = "© Mapzen | Transitland | Unfolding Maps | " + provider0Attrib; 746 | attribWidth = textWidth(attrib); 747 | } else if (key == 'q') { 748 | map.mapDisplay.setProvider(providerq); 749 | attrib = "© Mapzen | Transitland | Unfolding Maps | " + providerqAttrib; 750 | attribWidth = textWidth(attrib); 751 | } else if (key == 'w') { 752 | map.mapDisplay.setProvider(providerw); 753 | attrib = "© Mapzen | Transitland | Unfolding Maps | " + providerwAttrib; 754 | attribWidth = textWidth(attrib); 755 | } else if (key == 'e') { 756 | map.mapDisplay.setProvider(providere); 757 | attrib = "© Mapzen | Transitland | Unfolding Maps | " + providereAttrib; 758 | attribWidth = textWidth(attrib); 759 | } else if (key == 'r') { 760 | map.mapDisplay.setProvider(providerr); 761 | attrib = "© Mapzen | Transitland | Unfolding Maps | " + providerrAttrib; 762 | attribWidth = textWidth(attrib); 763 | } else if (key == 't') { 764 | map.mapDisplay.setProvider(providert); 765 | attrib = "© Mapzen | Transitland | Unfolding Maps | " + providertAttrib; 766 | attribWidth = textWidth(attrib); 767 | } else if (key == 'y') { 768 | map.mapDisplay.setProvider(providery); 769 | attrib = "© Mapzen | Transitland | Unfolding Maps | " + provideryAttrib; 770 | attribWidth = textWidth(attrib); 771 | } else if (key == 'u') { 772 | map.mapDisplay.setProvider(provideru); 773 | attrib = "© Mapzen | Transitland | Unfolding Maps | " + provideruAttrib; 774 | attribWidth = textWidth(attrib); 775 | } 776 | } 777 | 778 | class Line { 779 | float startx, starty, endx, endy; 780 | color c; 781 | Line(float _startx, float _starty, float _endx, float _endy, color _c) { 782 | startx = _startx; 783 | starty = _starty; 784 | endx = _endx; 785 | endy = _endy; 786 | c = _c; 787 | } 788 | void plot() { 789 | 790 | strokeWeight(0.5); 791 | stroke(c); 792 | line(startx, starty, endx, endy); 793 | } 794 | } 795 | 796 | 797 | class Trips { 798 | 799 | int tripFrames; 800 | int startFrame; 801 | int endFrame; 802 | Location start; 803 | Location end; 804 | Location currentLocation; 805 | ScreenPosition currentPosition; 806 | int s; 807 | float bearing; 808 | float radians; 809 | float xscale = 1.8; 810 | float yscale = 0.8; 811 | 812 | // class constructor 813 | Trips(int duration, int start_frame, int end_frame, Location startLocation, Location endLocation, float _bearing) { 814 | tripFrames = duration; 815 | startFrame = start_frame; 816 | endFrame = end_frame; 817 | start = startLocation; 818 | end = endLocation; 819 | bearing = _bearing; 820 | radians = radians(bearing); 821 | } 822 | 823 | // function to draw each trip 824 | void plotRide(){ 825 | if (frameCount >= startFrame && frameCount < endFrame){ 826 | float percentTravelled = (float(frameCount) - float(startFrame)) / float(tripFrames); 827 | 828 | currentLocation = new Location( 829 | 830 | lerp(start.x, end.x, percentTravelled), 831 | lerp(start.y, end.y, percentTravelled)); 832 | 833 | currentPosition = map.getScreenPosition(currentLocation); 834 | 835 | // Zoom dependent ellipse size 836 | float z = map.getZoom(); 837 | if (z <= 32.0){ s = 2; 838 | } else if (z == 64.0){ s = 2; 839 | } else if (z == 128.0){ s = 2; 840 | } else if (z == 256.0){ s = 3; 841 | } else if (z == 512.0){ s = 4; 842 | } else if (z == 1024.0){ s = 5; 843 | } else if (z == 2048.0){ s = 6; 844 | } else if (z == 4096.0){ s = 7; 845 | } else if (z == 8192.0){ s = 8; 846 | } else if (z >= 16384.0){ s = 10; 847 | } 848 | 849 | 850 | if (rotateBearing == false) ellipse(currentPosition.x, currentPosition.y, s, s); 851 | else { 852 | pushMatrix(); 853 | pushStyle(); 854 | translate(currentPosition.x, currentPosition.y); 855 | rotate(radians + PI/2); 856 | rectMode(CENTER); 857 | rect(0, 0, s*xscale, s*yscale, 7); 858 | popStyle(); 859 | popMatrix(); 860 | } 861 | } 862 | } 863 | 864 | void plotBus(){ 865 | if (frameCount >= startFrame && frameCount < endFrame){ 866 | float percentTravelled = (float(frameCount) - float(startFrame)) / float(tripFrames); 867 | 868 | currentLocation = new Location( 869 | 870 | lerp(start.x, end.x, percentTravelled), 871 | lerp(start.y, end.y, percentTravelled)); 872 | 873 | currentPosition = map.getScreenPosition(currentLocation); 874 | 875 | // Zoom dependent ellipse size 876 | float z = map.getZoom(); 877 | if (z <= 32.0){ s = 2; 878 | } else if (z == 64.0){ s = 2; 879 | } else if (z == 128.0){ s = 2; 880 | } else if (z == 256.0){ s = 3; 881 | } else if (z == 512.0){ s = 4; 882 | } else if (z == 1024.0){ s = 5; 883 | } else if (z == 2048.0){ s = 5; 884 | } else if (z == 4096.0){ s = 6; 885 | } else if (z == 8192.0){ s = 7; 886 | } else if (z >= 16384.0){ s = 9; 887 | } 888 | if (rotateBearing == false) ellipse(currentPosition.x, currentPosition.y, s, s); 889 | else { 890 | pushMatrix(); 891 | pushStyle(); 892 | translate(currentPosition.x, currentPosition.y); 893 | rotate(radians + PI/2); 894 | rectMode(CENTER); 895 | rect(0, 0, s*xscale, s*yscale, 7); 896 | popStyle(); 897 | popMatrix(); 898 | } 899 | } 900 | } 901 | 902 | void plotSubway(){ 903 | if (frameCount >= startFrame && frameCount < endFrame){ 904 | float percentTravelled = (float(frameCount) - float(startFrame)) / float(tripFrames); 905 | 906 | currentLocation = new Location( 907 | 908 | lerp(start.x, end.x, percentTravelled), 909 | lerp(start.y, end.y, percentTravelled)); 910 | 911 | currentPosition = map.getScreenPosition(currentLocation); 912 | 913 | // Zoom dependent ellipse size 914 | float z = map.getZoom(); 915 | if (z <= 32.0){ s = 3; 916 | } else if (z == 64.0){ s = 3; 917 | } else if (z == 128.0){ s = 3; 918 | } else if (z == 256.0){ s = 4; 919 | } else if (z == 512.0){ s = 5; 920 | } else if (z == 1024.0){ s = 7; 921 | } else if (z == 2048.0){ s = 8; 922 | } else if (z == 4096.0){ s = 9; 923 | } else if (z == 8192.0){ s = 10; 924 | } else if (z >= 16384.0){ s = 11; 925 | } 926 | 927 | if (rotateBearing == false) ellipse(currentPosition.x, currentPosition.y, s, s); 928 | else { 929 | pushMatrix(); 930 | pushStyle(); 931 | translate(currentPosition.x, currentPosition.y); 932 | rotate(radians + PI/2); 933 | rectMode(CENTER); 934 | rect(0, 0, s*xscale, s*yscale, 7); 935 | popStyle(); 936 | popMatrix(); 937 | } 938 | } 939 | } 940 | 941 | void plotTrain(){ 942 | if (frameCount >= startFrame && frameCount < endFrame){ 943 | float percentTravelled = (float(frameCount) - float(startFrame)) / float(tripFrames); 944 | 945 | currentLocation = new Location( 946 | 947 | lerp(start.x, end.x, percentTravelled), 948 | lerp(start.y, end.y, percentTravelled)); 949 | 950 | currentPosition = map.getScreenPosition(currentLocation); 951 | 952 | // Zoom dependent ellipse size 953 | float z = map.getZoom(); 954 | if (z <= 32.0){ s = 4; 955 | } else if (z == 64.0){ s = 4; 956 | } else if (z == 128.0){ s = 5; 957 | } else if (z == 256.0){ s = 6; 958 | } else if (z == 512.0){ s = 7; 959 | } else if (z == 1024.0){ s = 9; 960 | } else if (z == 2048.0){ s = 10; 961 | } else if (z == 4096.0){ s = 11; 962 | } else if (z == 8192.0){ s = 12; 963 | } else if (z >= 16384.0){ s = 13; 964 | } 965 | 966 | if (rotateBearing == false) ellipse(currentPosition.x, currentPosition.y, s, s); 967 | else { 968 | pushMatrix(); 969 | pushStyle(); 970 | translate(currentPosition.x, currentPosition.y); 971 | rotate(radians + PI/2); 972 | rectMode(CENTER); 973 | rect(0, 0, s*xscale, s*yscale, 7); 974 | popStyle(); 975 | popMatrix(); 976 | } 977 | } 978 | } 979 | 980 | void plotTram(){ 981 | if (frameCount >= startFrame && frameCount < endFrame){ 982 | float percentTravelled = (float(frameCount) - float(startFrame)) / float(tripFrames); 983 | 984 | currentLocation = new Location( 985 | 986 | lerp(start.x, end.x, percentTravelled), 987 | lerp(start.y, end.y, percentTravelled)); 988 | 989 | currentPosition = map.getScreenPosition(currentLocation); 990 | 991 | // Zoom dependent ellipse size 992 | float z = map.getZoom(); 993 | if (z <= 32.0){ s = 3; 994 | } else if (z == 64.0){ s = 3; 995 | } else if (z == 128.0){ s = 3; 996 | } else if (z == 256.0){ s = 4; 997 | } else if (z == 512.0){ s = 5; 998 | } else if (z == 1024.0){ s = 6; 999 | } else if (z == 2048.0){ s = 7; 1000 | } else if (z == 4096.0){ s = 8; 1001 | } else if (z == 8192.0){ s = 9; 1002 | } else if (z >= 16384.0){ s = 10; 1003 | } 1004 | 1005 | if (rotateBearing == false) ellipse(currentPosition.x, currentPosition.y, s, s); 1006 | else { 1007 | pushMatrix(); 1008 | pushStyle(); 1009 | translate(currentPosition.x, currentPosition.y); 1010 | rotate(radians + PI/2); 1011 | rectMode(CENTER); 1012 | rect(0, 0, s*xscale, s*yscale, 7); 1013 | popStyle(); 1014 | popMatrix(); 1015 | } 1016 | } 1017 | } 1018 | 1019 | void plotFerry(){ 1020 | if (frameCount >= startFrame && frameCount < endFrame){ 1021 | float percentTravelled = (float(frameCount) - float(startFrame)) / float(tripFrames); 1022 | 1023 | currentLocation = new Location( 1024 | 1025 | lerp(start.x, end.x, percentTravelled), 1026 | lerp(start.y, end.y, percentTravelled)); 1027 | 1028 | currentPosition = map.getScreenPosition(currentLocation); 1029 | 1030 | // Zoom dependent ellipse size 1031 | float z = map.getZoom(); 1032 | if (z <= 32.0){ s = 3; 1033 | } else if (z == 64.0){ s = 3; 1034 | } else if (z == 128.0){ s = 3; 1035 | } else if (z == 256.0){ s = 4; 1036 | } else if (z == 512.0){ s = 5; 1037 | } else if (z == 1024.0){ s = 6; 1038 | } else if (z == 2048.0){ s = 7; 1039 | } else if (z == 4096.0){ s = 8; 1040 | } else if (z == 8192.0){ s = 9; 1041 | } else if (z >= 16384.0){ s = 10; 1042 | } 1043 | 1044 | if (rotateBearing == false) ellipse(currentPosition.x, currentPosition.y, s, s); 1045 | else { 1046 | pushMatrix(); 1047 | pushStyle(); 1048 | translate(currentPosition.x, currentPosition.y); 1049 | rotate(radians + PI/2); 1050 | rectMode(CENTER); 1051 | rect(0, 0, s*xscale*0.8, s*yscale, 7); 1052 | popStyle(); 1053 | popMatrix(); 1054 | } 1055 | } 1056 | } 1057 | 1058 | } 1059 | -------------------------------------------------------------------------------- /transitflow/transitflow.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import numpy as np 3 | import datetime as dt 4 | import glob 5 | import os 6 | import argparse 7 | import math 8 | import shutil 9 | from string import Template 10 | 11 | from transitland_api import TransitlandRequest 12 | 13 | TLAPI = None 14 | OUTPUT_NAME = None 15 | DATE = None 16 | FRAMES = None 17 | PER_PAGE = 10000 18 | 19 | # Time 20 | def time_to_seconds(value): 21 | h, m, s = map(int, value.split(':')) 22 | return (h * 60 * 60) + (m * 60) + s 23 | 24 | def seconds_to_time(value): 25 | return '%02d:%02d:%02d'%(seconds_to_hms(value)) 26 | 27 | def seconds_to_hms(value): 28 | m, s = divmod(value, 60) 29 | h, m = divmod(m, 60) 30 | return h, m, s 31 | 32 | # Helper functions 33 | def get_vehicle_types(operator_onestop_id): 34 | """This function will get all **vehicle types** for an operator, by route. So we can ask *"what vehicle type is this particular trip?"* and color code trips by vehicle type.""" 35 | routes_request = TLAPI.request('routes', operated_by=operator_onestop_id, per_page=PER_PAGE) 36 | lookup_vehicle_types = {i['onestop_id']: i['vehicle_type'] for i in routes_request} 37 | print(len(lookup_vehicle_types.keys()), "routes found.\n") 38 | return lookup_vehicle_types 39 | 40 | # Get stops 41 | def get_stop_lat_lons(operator_onestop_id): 42 | """Get stop lats and stop lons for a particular operator.""" 43 | stops_request = TLAPI.request('stops', served_by=operator_onestop_id, per_page=PER_PAGE) 44 | lookup_stop_lats = {} 45 | lookup_stop_lons = {} 46 | for stop in stops_request: 47 | try: 48 | lookup_stop_lats[stop['onestop_id']] = stop['geometry']['coordinates'][1] 49 | lookup_stop_lons[stop['onestop_id']] = stop['geometry']['coordinates'][0] 50 | except: 51 | pass 52 | print(len(lookup_stop_lats.keys()), "stops found.\n") 53 | return lookup_stop_lats, lookup_stop_lons 54 | 55 | # Get Schedule data 56 | def get_schedule_stop_pairs(operator_onestop_id, date): 57 | """This function gets origin-destination pairs and timestamps from the schedule stop pairs API. This is the most important function and the largest API request.""" 58 | ssp_request = TLAPI.request('schedule_stop_pairs', operator_onestop_id=operator_onestop_id, date=date, per_page=PER_PAGE, sort_min_id=0) 59 | #print sum(1 for _ in ssp_request), "schedule stop pairs found." 60 | origin_times = [] 61 | destination_times = [] 62 | origin_stops = [] 63 | destination_stops = [] 64 | route_ids = [] 65 | count=0 66 | for i in ssp_request: 67 | count+=1 68 | if count % 10000 == 0: 69 | print(count) 70 | if i['frequency_start_time']: 71 | start = time_to_seconds(i['frequency_start_time']) 72 | now = start 73 | end = time_to_seconds(i['frequency_end_time']) 74 | incr = i['frequency_headway_seconds'] 75 | while now <= end: 76 | # print "freq: start %s now %s end %s incr %s"%(start, now, end, incr) 77 | odt = (time_to_seconds(i['origin_departure_time']) - start) + now 78 | dat = (time_to_seconds(i['destination_arrival_time']) - start) + now 79 | now += incr 80 | origin_times.append(seconds_to_time(odt)) 81 | destination_times.append(seconds_to_time(dat)) 82 | origin_stops.append(i['origin_onestop_id']) 83 | destination_stops.append(i['destination_onestop_id']) 84 | route_ids.append(i['route_onestop_id']) 85 | else: 86 | origin_times.append(i['origin_departure_time']) 87 | destination_times.append(i['destination_arrival_time']) 88 | origin_stops.append(i['origin_onestop_id']) 89 | destination_stops.append(i['destination_onestop_id']) 90 | route_ids.append(i['route_onestop_id']) 91 | print(count, "schedule stop pairs found.\n") 92 | return origin_times, destination_times, origin_stops, destination_stops, route_ids 93 | 94 | def calculate_durations(origin_times, destination_times): 95 | """This function calculates durations between origin and destination pairs (in seconds).""" 96 | origin_since_epoch = map(time_to_seconds, origin_times) 97 | destination_since_epoch = map(time_to_seconds, destination_times) 98 | durations = [b - a for a, b in zip(origin_since_epoch, destination_since_epoch)] 99 | return durations 100 | 101 | def clean_times(origin_times, destination_times): 102 | """This function cleans origin and destination times. This is a bit tricky because operators will often include non-real times such as "26:00:00" to indicate 2am the next day.""" 103 | # Modulo away the > 24 hours 104 | origin_times_clean = [":".join([str(int(i.split(':')[0]) % 24), i.split(':')[1], i.split(':')[2]]) for i in origin_times] 105 | destination_times_clean = [":".join([str(int(i.split(':')[0]) % 24), i.split(':')[1], i.split(':')[2]]) for i in destination_times] 106 | return origin_times_clean, destination_times_clean 107 | 108 | def add_dates(date, origin_times_clean, destination_times_clean): 109 | """This function appends destination and origin dates to times, so that times become datetimes.""" 110 | date1 = dt.datetime.strptime(date, "%Y-%m-%d").date() 111 | date2 = date1 + dt.timedelta(days=1) 112 | 113 | origin_datetimes = [] 114 | destination_datetimes = [] 115 | 116 | for i in range(len(origin_times_clean)): 117 | if int(destination_times_clean[i].split(":")[0]) < int(origin_times_clean[i].split(":")[0]): 118 | origin_datetime = str(date1) + " " + origin_times_clean[i] 119 | destination_datetime = str(date2) + " " + destination_times_clean[i] 120 | else: 121 | origin_datetime = str(date1) + " " + origin_times_clean[i] 122 | destination_datetime = str(date1) + " " + destination_times_clean[i] 123 | 124 | origin_datetimes.append(origin_datetime) 125 | destination_datetimes.append(destination_datetime) 126 | 127 | return origin_datetimes, destination_datetimes 128 | 129 | # Output 130 | def generate_output(operator_onestop_id, origin_datetimes, destination_datetimes, durations, origin_stops, destination_stops, route_ids, lookup_stop_lats, lookup_stop_lons, lookup_vehicle_types): 131 | """This function generates the output table, to be saved later as a csv.""" 132 | origin_stop_lats = [] 133 | origin_stop_lons = [] 134 | for i in origin_stops: 135 | try: 136 | origin_stop_lats.append(lookup_stop_lats[i]) 137 | origin_stop_lons.append(lookup_stop_lons[i]) 138 | except: 139 | origin_stop_lats.append(0) 140 | origin_stop_lons.append(0) 141 | 142 | destination_stop_lats = [] 143 | destination_stop_lons = [] 144 | for i in destination_stops: 145 | try: 146 | destination_stop_lats.append(lookup_stop_lats[i]) 147 | destination_stop_lons.append(lookup_stop_lons[i]) 148 | except: 149 | destination_stop_lats.append(0) 150 | destination_stop_lons.append(0) 151 | 152 | vehicle_types = [] 153 | for i in route_ids: 154 | try: 155 | vehicle_type = lookup_vehicle_types[i] 156 | vehicle_types.append(vehicle_type) 157 | except: 158 | vehicle_types.append("NA") 159 | 160 | output = pd.DataFrame({ 161 | 'route_type': vehicle_types, 162 | 'start_time': origin_datetimes, 163 | 'start_lat': origin_stop_lats, 164 | 'start_lon': origin_stop_lons, 165 | 'end_time': destination_datetimes, 166 | 'end_lat': destination_stop_lats, 167 | 'end_lon': destination_stop_lons, 168 | 'duration': durations 169 | }) 170 | output = output[['start_time', 'start_lat', 'start_lon', 'end_time', 'end_lat', 'end_lon', 'duration', 'route_type']] 171 | 172 | # drop rows where lat or lon = 0 173 | output = output[output['start_lat'] != 0] 174 | output = output[output['start_lon'] != 0] 175 | output = output[output['end_lat'] != 0] 176 | output = output[output['end_lon'] != 0] 177 | 178 | return output 179 | 180 | # Combine data 181 | def concatenate_csvs(path): 182 | all_files = glob.glob(os.path.join(path, "*.csv")) # advisable to use os.path.join as this makes concatenation OS independent 183 | df_from_each_file = (pd.read_csv(f) for f in all_files) # generators 184 | concatenated_df = pd.concat(df_from_each_file, ignore_index=True) 185 | del concatenated_df['Unnamed: 0'] # delete the blank column that gets added 186 | concatenated_df['start_time'] = pd.to_datetime(concatenated_df['start_time']) 187 | concatenated_df['end_time'] = pd.to_datetime(concatenated_df['end_time']) 188 | concatenated_df = concatenated_df.sort_values(by="start_time").reset_index(drop=True) 189 | return concatenated_df 190 | 191 | def animate_one_day(operator_onestop_id, date): 192 | """This is the main function that ties all of the above together!""" 193 | lookup_vehicle_types = get_vehicle_types(operator_onestop_id) 194 | lookup_stop_lats, lookup_stop_lons = get_stop_lat_lons(operator_onestop_id) 195 | origin_times, destination_times, origin_stops, destination_stops, route_ids = get_schedule_stop_pairs(operator_onestop_id, date) 196 | durations = calculate_durations(origin_times, destination_times) 197 | origin_times_clean, destination_times_clean = clean_times(origin_times, destination_times) 198 | origin_datetimes, destination_datetimes = add_dates(date, origin_times_clean, destination_times_clean) 199 | output = generate_output(operator_onestop_id, origin_datetimes, destination_datetimes, durations, origin_stops, destination_stops, route_ids, lookup_stop_lats, lookup_stop_lons, lookup_vehicle_types) 200 | output = output.sort_values(by='start_time').reset_index(drop=True) 201 | return output 202 | 203 | def animate_operators(operators, date): 204 | """Main.""" 205 | results = [] 206 | failures = [] 207 | 208 | length = len(operators) 209 | count = 1 210 | 211 | for i in operators: 212 | try: 213 | i = i.encode('utf-8') 214 | except: 215 | i = unicode(i, 'utf-8') 216 | i = i.encode('utf-8') 217 | print(i, count, "/", length) 218 | 219 | try: 220 | output = animate_one_day(i, date) 221 | results.append(output) 222 | print("success!") 223 | 224 | output.to_csv("sketches/{}/{}/data/indiv_operators/{}.csv".format(OUTPUT_NAME, DATE, i)) 225 | except Exception: 226 | failures.append(i) 227 | print("failed:") 228 | 229 | count += 1 230 | 231 | return results, failures 232 | 233 | # Calculate bearing 234 | # See: https://gis.stackexchange.com/questions/29239/calculate-bearing-between-two-decimal-gps-coordinates/48911 235 | def calc_bearing_between_points(startLat, startLong, endLat, endLong): 236 | 237 | startLat = math.radians(startLat) 238 | startLong = math.radians(startLong) 239 | endLat = math.radians(endLat) 240 | endLong = math.radians(endLong) 241 | dLong = endLong - startLong 242 | dPhi = math.log(math.tan(endLat/2.0+math.pi/4.0)/math.tan(startLat/2.0+math.pi/4.0)) 243 | if abs(dLong) > math.pi: 244 | if dLong > 0.0: 245 | dLong = -(2.0 * math.pi - dLong) 246 | else: 247 | dLong = (2.0 * math.pi + dLong) 248 | bearing = (math.degrees(math.atan2(dLong, dPhi)) + 360.0) % 360.0; 249 | return bearing 250 | 251 | # Stacked bar chart functions 252 | def count_vehicles_on_screen(concatenated_df, min_time, max_time, frames): 253 | number_of_vehicles = [] 254 | number_of_buses = [] 255 | number_of_trams = [] 256 | number_of_cablecars = [] 257 | number_of_metros = [] 258 | number_of_trains = [] 259 | number_of_ferries = [] 260 | 261 | time_range = max_time - min_time 262 | time_step = time_range / frames 263 | time_segments = [] 264 | 265 | for i in range(frames): 266 | time = min_time + i * time_step 267 | time_segments.append(time) 268 | 269 | for increment in time_segments: 270 | 271 | vehicles_on_the_road = concatenated_df[(concatenated_df['end_time'] > increment) & (concatenated_df['start_time'] <= increment)] 272 | number_vehicles_on_the_road = len(vehicles_on_the_road) 273 | number_of_vehicles.append(number_vehicles_on_the_road) 274 | 275 | for route_type in ['bus', 'tram', 'cablecar', 'metro', 'rail', 'ferry']: 276 | just_this_mode = vehicles_on_the_road[vehicles_on_the_road['route_type'] == route_type] 277 | number_of_this_mode = len(just_this_mode) 278 | if route_type == 'bus': 279 | number_of_buses.append(number_of_this_mode) 280 | elif route_type == 'tram': 281 | number_of_trams.append(number_of_this_mode) 282 | elif route_type == 'cablecar': 283 | number_of_cablecars.append(number_of_this_mode) 284 | elif route_type == 'metro': 285 | number_of_metros.append(number_of_this_mode) 286 | elif route_type == 'rail': 287 | number_of_trains.append(number_of_this_mode) 288 | elif route_type == 'ferry': 289 | number_of_ferries.append(number_of_this_mode) 290 | 291 | vehicles = pd.DataFrame(list(zip(time_segments, number_of_vehicles))) 292 | buses = pd.DataFrame(list(zip(time_segments, number_of_buses))) 293 | trams = pd.DataFrame(list(zip(time_segments, number_of_trams))) 294 | cablecars = pd.DataFrame(list(zip(time_segments, number_of_cablecars))) 295 | metros = pd.DataFrame(list(zip(time_segments, number_of_metros))) 296 | trains = pd.DataFrame(list(zip(time_segments, number_of_trains))) 297 | ferries = pd.DataFrame(list(zip(time_segments, number_of_ferries))) 298 | 299 | for df in [vehicles, buses, trams, metros, cablecars, trains, ferries]: 300 | df.columns = ['time', 'count'] 301 | 302 | return vehicles, buses, trams, metros, cablecars, trains, ferries 303 | 304 | if __name__ == "__main__": 305 | todays_date = str(dt.datetime.today()).split(" ")[0] 306 | 307 | parser = argparse.ArgumentParser() 308 | parser.add_argument("--date", default=todays_date, help="Animation day") 309 | parser.add_argument("--apikey", help="Mapzen API Key") 310 | parser.add_argument( 311 | "--name", 312 | help="Output directory name", 313 | default="output" 314 | ) 315 | parser.add_argument( 316 | "--bbox", 317 | help="Bounding box" 318 | ) 319 | parser.add_argument( 320 | "--frames", 321 | help="Number of frames in animation. 3600 frames = 60 second animation.", 322 | default=3600 323 | ) 324 | parser.add_argument( 325 | "--exclude", 326 | help="Exclude particular operators by operator onestop_id" 327 | ) 328 | parser.add_argument( 329 | "--operator", 330 | help="Download data for a single operator by operator onestop_id", 331 | ) 332 | parser.add_argument( 333 | "--clip_to_bbox", 334 | help="Clip trips to bounding box", 335 | action="store_true" 336 | ) 337 | parser.add_argument( 338 | "--recording", 339 | help="Records sketch to mp4", 340 | action="store_true" 341 | ) 342 | 343 | args = parser.parse_args() 344 | 345 | if not args.date: 346 | raise Exception('date required') 347 | 348 | OUTPUT_NAME = args.name 349 | DATE = args.date 350 | FRAMES = args.frames 351 | RECORDING = args.recording 352 | 353 | TLAPI = TransitlandRequest( 354 | host='http://transit.land', 355 | apikey=args.apikey 356 | ) 357 | 358 | 359 | print("INPUTS:") 360 | print("date: ", DATE) 361 | print("name: ", OUTPUT_NAME) 362 | print("API key: ", args.apikey) 363 | 364 | timer_start = dt.datetime.now() 365 | 366 | if args.bbox: 367 | west, south, east, north = args.bbox.split(",") 368 | # west, south, east, north = args.bbox.split(",") 369 | # bbox = true 370 | 371 | operators = set() 372 | if args.operator: 373 | operators |= set(args.operator.split(",")) 374 | 375 | exclude_operators = set() 376 | if args.exclude: 377 | exclude_operators |= set(args.exclude.split(",")) 378 | print("exclude: ", list(exclude_operators)) 379 | 380 | if args.bbox: 381 | print("bbox: ", west, south, east, north) 382 | 383 | # First, let's get a list of the onestop id's for every operator in our bounding box. 384 | operators_request = TLAPI.request('operators', bbox=','.join([west,south,east,north]), per_page=PER_PAGE) 385 | operators_in_bbox = {i['onestop_id'] for i in operators_request} 386 | print(len(operators_in_bbox), "operators in bounding box.") 387 | operators |= operators_in_bbox 388 | 389 | # Operators to be excluded from viz and stats 390 | operators -= exclude_operators 391 | print(len(operators), "operators to be downloaded.") 392 | 393 | ############ 394 | 395 | if not os.path.exists("sketches/{}/{}/data/indiv_operators".format(OUTPUT_NAME, DATE)): 396 | os.makedirs("sketches/{}/{}/data/indiv_operators".format(OUTPUT_NAME, DATE)) 397 | results, failures = animate_operators(operators, DATE) 398 | print(len(results), "operators successfully downloaded.") 399 | print(len(failures), "operators failed.") 400 | if len(failures): print("failed operators:", failures) 401 | 402 | # ### Concatenate all individual operator csv files into one big dataframe 403 | print("Concatenating individual operator outputs.") 404 | df = concatenate_csvs("sketches/{}/{}/data/indiv_operators".format(OUTPUT_NAME, DATE)) 405 | print("Calculating trip segment bearings.") 406 | df['bearing'] = df.apply(lambda row: calc_bearing_between_points(row['start_lat'], row['start_lon'], row['end_lat'], row['end_lon']), axis=1) 407 | 408 | # Clip to bbox. Either the start stop is within the bbox OR the end stop is within the bbox 409 | if args.bbox and args.clip_to_bbox: 410 | df = df[ 411 | ((df['start_lat'] >= float(south)) & (df['start_lat'] <= float(north)) & (df['start_lon'] >= float(west)) & (df['start_lon'] <= float(east))) | 412 | ((df['end_lat'] >= float(south)) & (df['end_lat'] <= float(north)) & (df['end_lon'] >= float(west)) & (df['end_lon'] <= float(east))) 413 | ] 414 | 415 | # Save to csv. 416 | df.to_csv("sketches/{}/{}/data/output.csv".format(OUTPUT_NAME, DATE)) 417 | print("Total rows: ", df.shape[0]) 418 | 419 | # ### That's it for the trip data! 420 | 421 | # ### Next step: Count number of vehicles in transit at every 15 second interval 422 | # In order to add a stacked area chart to the animation showing the number 423 | # of vehicles on the road, we will do some counting here in python and save 424 | # the results in six separate csv files (one for each mode of transit in 425 | # SF Bay Area: bus, tram, cablecar, metro, rail, ferry. 426 | # The Processing sketch will read in each file and use them to plot a 427 | # stacked area chart. 428 | 429 | min_time = min(df['start_time']) 430 | max_time = max(df['end_time']) 431 | 432 | print("Counting number of vehicles in transit.") 433 | vehicles, buses, trams, metros, cablecars, trains, ferries = count_vehicles_on_screen(df, min_time, max_time, FRAMES) 434 | 435 | random_indices = np.sort(np.random.choice(vehicles.index, int(FRAMES), replace=False)) 436 | 437 | vehicles_counts_output = vehicles.loc[random_indices].reset_index(drop=True) 438 | vehicles_counts_output['frame'] = vehicles_counts_output.index 439 | buses_counts_output = buses.loc[random_indices].reset_index(drop=True) 440 | buses_counts_output['frame'] = buses_counts_output.index 441 | trams_counts_output = trams.loc[random_indices].reset_index(drop=True) 442 | trams_counts_output['frame'] = trams_counts_output.index 443 | metros_counts_output = metros.loc[random_indices].reset_index(drop=True) 444 | metros_counts_output['frame'] = metros_counts_output.index 445 | cablecars_counts_output = cablecars.loc[random_indices].reset_index(drop=True) 446 | cablecars_counts_output['frame'] = cablecars_counts_output.index 447 | trains_counts_output = trains.loc[random_indices].reset_index(drop=True) 448 | trains_counts_output['frame'] = trains_counts_output.index 449 | ferries_counts_output = ferries.loc[random_indices].reset_index(drop=True) 450 | ferries_counts_output['frame'] = ferries_counts_output.index 451 | 452 | # Save these vehicle count stats to csv's. 453 | if not os.path.exists("sketches/{}/{}/data/vehicle_counts".format(OUTPUT_NAME, DATE)): 454 | os.makedirs("sketches/{}/{}/data/vehicle_counts".format(OUTPUT_NAME, DATE)) 455 | vehicles_counts_output.to_csv("sketches/{}/{}/data/vehicle_counts/vehicles_{}.csv".format(OUTPUT_NAME, DATE, FRAMES)) 456 | buses_counts_output.to_csv("sketches/{}/{}/data/vehicle_counts/buses_{}.csv".format(OUTPUT_NAME, DATE, FRAMES)) 457 | trams_counts_output.to_csv("sketches/{}/{}/data/vehicle_counts/trams_{}.csv".format(OUTPUT_NAME, DATE, FRAMES)) 458 | metros_counts_output.to_csv("sketches/{}/{}/data/vehicle_counts/metros_{}.csv".format(OUTPUT_NAME, DATE, FRAMES)) 459 | cablecars_counts_output.to_csv("sketches/{}/{}/data/vehicle_counts/cablecars_{}.csv".format(OUTPUT_NAME, DATE, FRAMES)) 460 | trains_counts_output.to_csv("sketches/{}/{}/data/vehicle_counts/trains_{}.csv".format(OUTPUT_NAME, DATE, FRAMES)) 461 | ferries_counts_output.to_csv("sketches/{}/{}/data/vehicle_counts/ferries_{}.csv".format(OUTPUT_NAME, DATE, FRAMES)) 462 | 463 | # Hacky way to center the sketch 464 | if not args.bbox: 465 | south, west, north, east = df['start_lat'][0], df['start_lon'][0], df['start_lat'][1], df['start_lon'][1] 466 | 467 | ## Use processing sketch template to create processing sketch file 468 | module_path = os.path.join(os.path.dirname(__file__)) 469 | template_path = os.path.join(module_path, 'templates', 'template.pde') 470 | with open(template_path) as f: 471 | data = f.read() 472 | s = Template(data) 473 | 474 | if not os.path.exists("sketches/{}/{}/sketch".format(OUTPUT_NAME, DATE)): 475 | os.makedirs("sketches/{}/{}/sketch".format(OUTPUT_NAME, DATE)) 476 | 477 | for asset in ['calendar_icon.png', 'clock_icon.png']: 478 | shutil.copyfile( 479 | os.path.join(module_path, 'assets', asset), 480 | os.path.join('sketches', OUTPUT_NAME, DATE, "sketch", asset) 481 | ) 482 | 483 | with open("sketches/{}/{}/sketch/sketch.pde".format(OUTPUT_NAME, DATE), "w") as f: 484 | f.write( 485 | s.substitute( 486 | DIRECTORY_NAME=OUTPUT_NAME, 487 | DATE=DATE, 488 | TOTAL_FRAMES=FRAMES, 489 | RECORDING=str(RECORDING).lower(), 490 | AVG_LAT=(float(south) + float(north))/2.0, 491 | AVG_LON=(float(west) + float(east))/2.0 492 | ) 493 | ) 494 | 495 | timer_finish = dt.datetime.now() 496 | time_delta = timer_finish - timer_start 497 | print("Time elapsed: ", str(time_delta)) 498 | -------------------------------------------------------------------------------- /transitflow/transitland_api.py: -------------------------------------------------------------------------------- 1 | import time 2 | import urllib 3 | import urllib.request 4 | import json 5 | 6 | class TransitlandRequest(object): 7 | """Simple transitland API interface, with rate limits.""" 8 | last_request_time = 0.0 # time of last request 9 | 10 | def __init__(self, host='https://transit.land', apikey=None, ratelimit=8, retrylimit=5): 11 | self.host = host 12 | self.apikey = apikey 13 | self.ratelimit = ratelimit 14 | self.retrylimit = retrylimit 15 | 16 | def wait_time(self): 17 | now = time.time() 18 | wait_time = (1.0 / self.ratelimit) - (now - self.last_request_time) 19 | # print "ratelimit: ", self.ratelimit, " /s: ", (1.0 / self.ratelimit) 20 | # print "last_request_time: ", self.last_request_time 21 | # print "now: ", now 22 | # print "dt: ", (now - self.last_request_time) 23 | # print "wait_time: ", wait_time 24 | if wait_time > 0: 25 | time.sleep(wait_time) 26 | self.last_request_time = now 27 | 28 | def _request(self, uri, retries=0): 29 | print(uri) 30 | success = False 31 | data = {} 32 | while not success: 33 | self.wait_time() 34 | try: 35 | req = urllib.request.Request(uri) 36 | req.add_header('Content-Type', 'application/json') 37 | response = urllib.request.urlopen(req) 38 | if response.getcode() >= 400: 39 | raise Exception('http error: %s'%(response.getcode())) 40 | else: 41 | success = True 42 | data = json.loads(response.read()) 43 | except Exception: 44 | retries += 1 45 | if retries > self.retrylimit: 46 | raise e 47 | print ("retry %s / %s: %s"%(retries, self.retrylimit)) 48 | return data 49 | 50 | def request(self, endpoint, **data): 51 | """Request with JSON response.""" 52 | # Create uri 53 | data = data or {} 54 | if self.apikey: 55 | data['apikey'] = self.apikey 56 | 57 | next_uri = '%s/api/v1/%s?%s'%(self.host, endpoint, urllib.parse.urlencode(data)) 58 | 59 | # Pagination 60 | while next_uri: 61 | data = self._request(next_uri) 62 | meta = data.get('meta', {}) 63 | next_uri = meta.get('next') 64 | # Temporary fix for pagination missing apikey 65 | if next_uri and self.apikey and ('apikey=' not in next_uri): 66 | next_uri = "%s&apikey=%s"%(next_uri, self.apikey) 67 | # transitland responses will have one main key that isn't "meta" 68 | main_key = (set(data.keys()) - set(['meta'])).pop() 69 | for item in data[main_key]: 70 | yield item 71 | --------------------------------------------------------------------------------