├── .gitignore
├── README.md
├── bundle.js
├── cleaned
├── nyc-streets.geo.json
├── nyc-streets.txt
├── stations.csv
├── trips-2017-04-01.csv
├── trips-2017-04-18.csv
├── trips-2017-05-21.csv
├── trips-2017-05-22.csv
├── trips-2017-06-03.csv
├── trips-2017-06-04.csv
├── trips-2017-06-05.csv
├── trips-2017-06-09.csv
├── trips-2017-07-15.csv
├── trips-2017-07-16.csv
└── trips-2017-07-17.csv
├── images
├── citibike-trips.png
├── citibike-trips.sketch
├── citibike.mov
└── design.png
├── index.html
├── old-code
└── main.py
├── package-lock.json
├── package.json
├── scripts
├── clean-citibike-trips.js
├── download-data.sh
├── extract-coordinates-from-geojson.js
└── process-trips.sh
├── src
├── check-support.js
├── create-buttons.js
├── create-elapsed-time-view.js
├── create-map-renderer.js
├── create-projection.js
├── create-roaming-camera.js
├── create-state-transitioner.js
├── create-timeline.js
├── create-trip-paths-renderer.js
├── create-trip-points-renderer.js
├── helpers.js
├── index.js
├── map.vert
├── pointer-circle.vert
├── setup-dat-gui.js
├── simple.frag
├── simple.vert
├── trip-path.frag
├── trip-path.vert
├── trip-points.frag
├── trip-points.vert
├── trip-state.frag
└── trip-state.vert
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | data
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # citibike-trips
2 |
3 | 
4 |
5 | ```
6 | npm run serve
7 | ```
8 |
9 | For dev:
10 | ```
11 | npm install
12 | npm start
13 | ```
14 |
15 | Playing with some citibike trip data made available at [https://www.citibikenyc.com/system-data](https://www.citibikenyc.com/system-data).
16 |
17 | Most of the dataset was too large for the repo, so you'll have to go download the data for yourself.
18 |
--------------------------------------------------------------------------------
/cleaned/stations.csv:
--------------------------------------------------------------------------------
1 | station_id,latitude,longitude,name
2 | 72,40.76727216,-73.99392888,"W 52 St & 11 Ave"
3 | 79,40.71911552,-74.00666661,"Franklin St & W Broadway"
4 | 82,40.71117416,-74.00016545,"St James Pl & Pearl St"
5 | 83,40.68382604,-73.97632328,"Atlantic Ave & Fort Greene Pl"
6 | 116,40.74177603,-74.00149746,"W 17 St & 8 Ave"
7 | 119,40.69608941,-73.97803415,"Park Ave & St Edwards St"
8 | 120,40.68676793,-73.95928168,"Lexington Ave & Classon Ave"
9 | 127,40.73172428,-74.00674436,"Barrow St & Hudson St"
10 | 128,40.72710258,-74.00297088,"MacDougal St & Prince St"
11 | 143,40.69239502,-73.99337909,"Clinton St & Joralemon St"
12 | 144,40.69839895,-73.98068914,"Nassau St & Navy St"
13 | 146,40.71625008,-74.0091059,"Hudson St & Reade St"
14 | 150,40.7208736,-73.98085795,"E 2 St & Avenue C"
15 | 151,40.722103786686034,-73.99724900722504,"Cleveland Pl & Spring St"
16 | 152,40.71473993,-74.00910627,"Warren St & Church St"
17 | 153,40.752062307,-73.9816324043,"E 40 St & 5 Ave"
18 | 157,40.69089272,-73.99612349,"Henry St & Atlantic Ave"
19 | 161,40.72917025,-73.99810231,"LaGuardia Pl & W 3 St"
20 | 164,40.75323098,-73.97032517,"E 47 St & 2 Ave"
21 | 167,40.7489006,-73.97604882,"E 39 St & 3 Ave"
22 | 168,40.73971301,-73.99456405,"W 18 St & 6 Ave"
23 | 173,40.76068327096592,-73.9845272898674,"Broadway & W 49 St"
24 | 174,40.7381765,-73.97738662,"E 25 St & 1 Ave"
25 | 195,40.70905623,-74.01043382,"Liberty St & Broadway"
26 | 212,40.74334935,-74.00681753,"W 16 St & The High Line"
27 | 216,40.70037867,-73.99548059,"Columbia Heights & Cranberry St"
28 | 217,40.70277159,-73.99383605,"Old Fulton St"
29 | 223,40.73781509,-73.99994661,"W 13 St & 7 Ave"
30 | 228,40.7546011026,-73.971878855,"E 48 St & 3 Ave"
31 | 229,40.72743423,-73.99379025,"Great Jones St"
32 | 232,40.69597683,-73.99014892,"Cadman Plaza E & Tillary St"
33 | 236,40.7284186,-73.98713956,"St Marks Pl & 2 Ave"
34 | 237,40.73047309,-73.98672378,"E 11 St & 2 Ave"
35 | 238,40.7361967,-74.00859207,"Bank St & Washington St"
36 | 239,40.69196566,-73.9813018,"Willoughby St & Fleet St"
37 | 241,40.68981035,-73.97493121,"DeKalb Ave & S Portland Ave"
38 | 242,40.697787,-73.973736,"Carlton Ave & Flushing Ave"
39 | 243,40.688226,-73.979382,"Fulton St & Rockwell Pl"
40 | 244,40.69196035,-73.96536851,"Willoughby Ave & Hall St"
41 | 245,40.69327018,-73.97703874,"Myrtle Ave & St Edwards St"
42 | 247,40.73535398,-74.00483091,"Perry St & Bleecker St"
43 | 248,40.72185379,-74.00771779,"Laight St & Hudson St"
44 | 249,40.71870987,-74.0090009,"Harrison St & Hudson St"
45 | 251,40.72317958,-73.99480012,"Mott St & Prince St"
46 | 252,40.73226398,-73.99852205,"MacDougal St & Washington Sq"
47 | 253,40.73543934,-73.99453948,"W 13 St & 5 Ave"
48 | 254,40.73532427,-73.99800419,"W 11 St & 6 Ave"
49 | 257,40.71939226,-74.00247214,"Lispenard St & Broadway"
50 | 258,40.68940747,-73.96885458,"DeKalb Ave & Vanderbilt Ave"
51 | 259,40.70122128,-74.01234218,"South St & Whitehall St"
52 | 260,40.70365182,-74.01167797,"Broad St & Bridge St"
53 | 261,40.69474881,-73.98362464,"Johnson St & Gold St"
54 | 262,40.6917823,-73.9737299,"Washington Park"
55 | 264,40.70706456,-74.00731853,"Maiden Ln & Pearl St"
56 | 265,40.72229346,-73.99147535,"Stanton St & Chrystie St"
57 | 266,40.72368361,-73.97574813,"Avenue D & E 8 St"
58 | 267,40.75097711,-73.98765428,"Broadway & W 36 St"
59 | 268,40.71910537,-73.99973337,"Howard St & Centre St"
60 | 270,40.69308257,-73.97178913,"Adelphi St & Myrtle Ave"
61 | 274,40.68691865,-73.976682,"Lafayette Ave & Fort Greene Pl"
62 | 275,40.68650065,-73.96563307,"Washington Ave & Greene Ave"
63 | 276,40.71748752,-74.0104554,"Duane St & Greenwich St"
64 | 278,40.69766564,-73.98476437,"Concord St & Bridge St"
65 | 279,40.707873,-74.00167,"Peck Slip & Front St"
66 | 280,40.73331967,-73.99510132,"E 10 St & 5 Ave"
67 | 281,40.7643971,-73.97371465,"Grand Army Plaza & Central Park S"
68 | 282,40.707644944175705,-73.96841526031494,"Kent Ave & S 11 St"
69 | 284,40.7390169121,-74.0026376103,"Greenwich Ave & 8 Ave"
70 | 285,40.73454567,-73.99074142,"Broadway & E 14 St"
71 | 289,40.6845683,-73.95881081,"Monroe St & Classon Ave"
72 | 291,40.713126,-73.984844,"Madison St & Montgomery St"
73 | 293,40.73020660529954,-73.99102628231049,"Lafayette St & E 8 St"
74 | 295,40.71406667,-73.99293911,"Pike St & E Broadway"
75 | 296,40.71413089,-73.9970468,"Division St & Bowery"
76 | 297,40.734232,-73.986923,"E 15 St & 3 Ave"
77 | 301,40.72217444,-73.98368779,"E 2 St & Avenue B"
78 | 302,40.72082834,-73.97793172,"Avenue D & E 3 St"
79 | 303,40.72362738,-73.99949601,"Mercer St & Spring St"
80 | 304,40.70463334,-74.01361706,"Broadway & Battery Pl"
81 | 305,40.76095756,-73.96724467,"E 58 St & 3 Ave"
82 | 306,40.70823502,-74.00530063,"Cliff St & Fulton St"
83 | 308,40.71307916,-73.99851193,"St James Pl & Oliver St"
84 | 309,40.7149787,-74.013012,"Murray St & West St"
85 | 310,40.68926942,-73.98912867,"State St & Smith St"
86 | 311,40.7172274,-73.98802084,"Norfolk St & Broome St"
87 | 312,40.722055,-73.989111,"Allen St & Stanton St"
88 | 313,40.69610226,-73.96751037,"Washington Ave & Park Ave"
89 | 314,40.69383,-73.990539,"Cadman Plaza West & Montague St"
90 | 315,40.70355377,-74.00670227,"South St & Gouverneur Ln"
91 | 316,40.70955958,-74.00653609,"Fulton St & William St"
92 | 317,40.72453734,-73.98185424,"E 6 St & Avenue B"
93 | 319,40.711066,-74.009447,"Fulton St & Broadway"
94 | 320,40.717571,-74.005549,"Leonard St & Church St"
95 | 321,40.69991755,-73.98971773,"Cadman Plaza E & Red Cross Pl"
96 | 322,40.696192,-73.991218,"Clinton St & Tillary St"
97 | 323,40.69236178,-73.98631746,"Lawrence St & Willoughby St"
98 | 324,40.689888,-73.981013,"DeKalb Ave & Hudson Ave"
99 | 325,40.73624527,-73.98473765,"E 19 St & 3 Ave"
100 | 326,40.72953837,-73.98426726,"E 11 St & 1 Ave"
101 | 327,40.7153379,-74.01658354,"Vesey Pl & River Terrace"
102 | 328,40.72405549,-74.00965965,"Watts St & Greenwich St"
103 | 330,40.71450451,-74.00562789,"Reade St & Broadway"
104 | 331,40.71173107,-73.99193043,"Pike St & Monroe St"
105 | 332,40.71219906,-73.97948148,"Cherry St"
106 | 334,40.74238787,-73.99726235,"W 20 St & 7 Ave"
107 | 335,40.72903917,-73.99404649,"Washington Pl & Broadway"
108 | 336,40.73047747,-73.99906065,"Sullivan St & Washington Sq"
109 | 337,40.7037992,-74.00838676,"Old Slip & Front St"
110 | 339,40.72580614,-73.97422494,"Avenue D & E 12 St"
111 | 341,40.71782143,-73.97628939,"Stanton St & Mangin St"
112 | 342,40.71739973,-73.98016555,"Columbia St & Rivington St"
113 | 343,40.69794,-73.96986848,"Clinton Ave & Flushing Ave"
114 | 344,40.6851443,-73.95380904,"Monroe St & Bedford Ave"
115 | 345,40.73649403,-73.99704374,"W 13 St & 6 Ave"
116 | 346,40.73652889,-74.00618026,"Bank St & Hudson St"
117 | 347,40.728846,-74.008591,"Greenwich St & W Houston St"
118 | 348,40.72490985,-74.00154702,"W Broadway & Spring St"
119 | 349,40.71850211,-73.98329859,"Rivington St & Ridge St"
120 | 350,40.71559509,-73.9870295,"Clinton St & Grand St"
121 | 351,40.70530954,-74.00612572,"Front St & Maiden Ln"
122 | 353,40.68539567,-73.97431458,"S Portland Ave & Hanson Pl"
123 | 354,40.69363137,-73.96223558,"Emerson Pl & Myrtle Ave"
124 | 355,40.71602118,-73.99974372,"Bayard St & Baxter St"
125 | 356,40.71622644,-73.98261206,"Bialystoker Pl & Delancey St"
126 | 357,40.73261787,-73.99158043,"E 11 St & Broadway"
127 | 358,40.73291553,-74.00711384,"Christopher St & Greenwich St"
128 | 359,40.75510267,-73.97498696,"E 47 St & Park Ave"
129 | 360,40.70717936,-74.00887308,"William St & Pine St"
130 | 361,40.71605866,-73.99190759,"Allen St & Hester St"
131 | 363,40.70834698,-74.01713445,"West Thames St"
132 | 364,40.68900443,-73.96023854,"Lafayette Ave & Classon Ave"
133 | 365,40.68223166,-73.9614583,"Fulton St & Grand Ave"
134 | 366,40.693261,-73.968896,"Clinton Ave & Myrtle Ave"
135 | 368,40.73038599,-74.00214988,"Carmine St & 6 Ave"
136 | 369,40.73224119,-74.00026394,"Washington Pl & 6 Ave"
137 | 372,40.694528,-73.958089,"Franklin Ave & Myrtle Ave"
138 | 373,40.69331716,-73.95381995,"Willoughby Ave & Walworth St"
139 | 376,40.70862144,-74.00722156,"John St & William St"
140 | 377,40.72243797,-74.00566443,"6 Ave & Canal St"
141 | 379,40.749156,-73.9916,"W 31 St & 7 Ave"
142 | 380,40.73401143,-74.00293877,"W 4 St & 7 Ave S"
143 | 382,40.73492695,-73.99200509,"University Pl & E 14 St"
144 | 383,40.735238,-74.000271,"Greenwich Ave & Charles St"
145 | 384,40.683048,-73.964915,"Fulton St & Washington Ave"
146 | 385,40.75797322,-73.96603308,"E 55 St & 2 Ave"
147 | 386,40.71494807,-74.00234482,"Centre St & Worth St"
148 | 387,40.71273266,-74.0046073,"Centre St & Chambers St"
149 | 388,40.749717753,-74.002950346,"W 26 St & 10 Ave"
150 | 389,40.71044554,-73.96525063,"Broadway & Berry St"
151 | 390,40.69221589,-73.9842844,"Duffield St & Willoughby St"
152 | 391,40.69760127,-73.99344559,"Clark St & Henry St"
153 | 392,40.695065,-73.987167,"Jay St & Tech Pl"
154 | 393,40.72299208,-73.97995466,"E 5 St & Avenue C"
155 | 394,40.72521311,-73.97768752,"E 9 St & Avenue C"
156 | 395,40.68807003,-73.98410637,"Bond St & Schermerhorn St"
157 | 396,40.680342423,-73.9557689392,"Lefferts Pl & Franklin Ave"
158 | 397,40.68415748,-73.96922273,"Fulton St & Clermont Ave"
159 | 398,40.69165183,-73.9999786,"Atlantic Ave & Furman St"
160 | 399,40.68851534,-73.9647628,"Lafayette Ave & St James Pl"
161 | 400,40.71926081,-73.98178024,"Pitt St & Stanton St"
162 | 401,40.72019576,-73.98997825,"Allen St & Rivington St"
163 | 402,40.7403432,-73.98955109,"Broadway & E 22 St"
164 | 403,40.72502876,-73.99069656,"E 2 St & 2 Ave"
165 | 405,40.739323,-74.008119,"Washington St & Gansevoort St"
166 | 406,40.69512845,-73.99595065,"Hicks St & Montague St"
167 | 407,40.700469,-73.991454,"Henry St & Poplar St"
168 | 408,40.71076228,-73.99400398,"Market St & Cherry St"
169 | 409,40.6906495,-73.95643107,"DeKalb Ave & Skillman St"
170 | 410,40.72066442,-73.98517977,"Suffolk St & Stanton St"
171 | 411,40.72228087,-73.97668709,"E 6 St & Avenue D"
172 | 412,40.7158155,-73.99422366,"Forsyth St & Canal St"
173 | 414,40.70281858,-73.98765762,"Pearl St & Anchorage Pl"
174 | 415,40.7047177,-74.00926027,"Pearl St & Hanover Square"
175 | 416,40.68753406,-73.97265183,"Cumberland St & Lafayette Ave"
176 | 417,40.71291224,-74.01020234,"Barclay St & Church St"
177 | 418,40.70224,-73.982578,"Front St & Gold St"
178 | 419,40.69580705,-73.97355569,"Carlton Ave & Park Ave"
179 | 420,40.68764484,-73.96968902,"Clermont Ave & Lafayette Ave"
180 | 421,40.69573398,-73.97129668,"Clermont Ave & Park Ave"
181 | 422,40.770513,-73.988038,"W 59 St & 10 Ave"
182 | 423,40.76584941,-73.98690506,"W 54 St & 9 Ave"
183 | 426,40.71754834,-74.01322069,"West St & Chambers St"
184 | 427,40.701907,-74.013942,"Bus Slip & State St"
185 | 428,40.72467721,-73.98783413,"E 3 St & 1 Ave"
186 | 430,40.7014851,-73.98656928,"York St & Jay St"
187 | 432,40.72621788,-73.98379855,"E 7 St & Avenue A"
188 | 433,40.72955361,-73.98057249,"E 13 St & Avenue A"
189 | 434,40.74317449,-74.00366443,"9 Ave & W 18 St"
190 | 435,40.74173969,-73.99415556,"W 21 St & 6 Ave"
191 | 436,40.68216564,-73.95399026,"Hancock St & Bedford Ave"
192 | 437,40.6809833854,-73.9500479759,"Macon St & Nostrand Ave"
193 | 438,40.72779126,-73.98564945,"St Marks Pl & 1 Ave"
194 | 439,40.7262807,-73.98978041,"E 4 St & 2 Ave"
195 | 440,40.75255434,-73.97282625,"E 45 St & 3 Ave"
196 | 441,40.756014,-73.967416,"E 52 St & 2 Ave"
197 | 442,40.746647,-73.993915,"W 27 St & 7 Ave"
198 | 443,40.70853074,-73.96408963,"Bedford Ave & S 9 St"
199 | 444,40.7423543,-73.98915076,"Broadway & W 24 St"
200 | 445,40.72740794,-73.98142006,"E 10 St & Avenue A"
201 | 446,40.74487634,-73.99529885,"W 24 St & 7 Ave"
202 | 447,40.76370739,-73.9851615,"8 Ave & W 52 St"
203 | 448,40.75660359,-73.9979009,"W 37 St & 10 Ave"
204 | 449,40.76461837,-73.98789473,"W 52 St & 9 Ave"
205 | 450,40.76227205,-73.98788205,"W 49 St & 8 Ave"
206 | 453,40.74475148,-73.99915362,"W 22 St & 8 Ave"
207 | 454,40.75455731,-73.96592976,"E 51 St & 1 Ave"
208 | 455,40.75001986,-73.96905301,"1 Ave & E 44 St"
209 | 456,40.7597108,-73.97402311,"E 53 St & Madison Ave"
210 | 457,40.76695317,-73.98169333,"Broadway & W 58 St"
211 | 458,40.751396,-74.005226,"11 Ave & W 27 St"
212 | 459,40.746745,-74.007756,"W 20 St & 11 Ave"
213 | 460,40.71285887,-73.96590294,"S 4 St & Wythe Ave"
214 | 461,40.73587678,-73.98205027,"E 20 St & 2 Ave"
215 | 462,40.74691959,-74.00451887,"W 22 St & 10 Ave"
216 | 465,40.75513557,-73.98658032,"Broadway & W 41 St"
217 | 466,40.74395411,-73.99144871,"W 25 St & 6 Ave"
218 | 467,40.68312489,-73.97895137,"Dean St & 4 Ave"
219 | 468,40.7652654,-73.98192338,"Broadway & W 56 St"
220 | 469,40.76344058,-73.98268129,"Broadway & W 53 St"
221 | 470,40.74345335,-74.00004031,"W 20 St & 8 Ave"
222 | 471,40.71286844,-73.95698119,"Grand St & Havemeyer St"
223 | 472,40.7457121,-73.98194829,"E 32 St & Park Ave"
224 | 473,40.72110063,-73.9919254,"Rivington St & Chrystie St"
225 | 474,40.7451677,-73.98683077,"5 Ave & E 29 St"
226 | 476,40.74394314,-73.97966069,"E 31 St & 3 Ave"
227 | 477,40.75640548,-73.9900262,"W 41 St & 8 Ave"
228 | 478,40.76030096,-73.99884222,"11 Ave & W 41 St"
229 | 479,40.76019252,-73.9912551,"9 Ave & W 45 St"
230 | 480,40.76669671,-73.99061728,"W 53 St & 10 Ave"
231 | 481,40.71260486,-73.96264403,"S 3 St & Bedford Ave"
232 | 482,40.73935542,-73.99931783,"W 15 St & 7 Ave"
233 | 483,40.73223272,-73.98889957,"E 12 St & 3 Ave"
234 | 484,40.75500254,-73.98014437,"W 44 St & 5 Ave"
235 | 485,40.75038009,-73.98338988,"W 37 St & 5 Ave"
236 | 486,40.7462009,-73.98855723,"Broadway & W 29 St"
237 | 487,40.73314259,-73.97573881,"E 20 St & FDR Drive"
238 | 490,40.751551,-73.993934,"8 Ave & W 33 St"
239 | 491,40.74096374,-73.98602213,"E 24 St & Park Ave S"
240 | 492,40.75019995,-73.99093085,"W 33 St & 7 Ave"
241 | 494,40.74734825,-73.99723551,"W 26 St & 8 Ave"
242 | 495,40.76269882,-73.99301222,"W 47 St & 10 Ave"
243 | 496,40.73726186,-73.99238967,"E 16 St & 5 Ave"
244 | 497,40.73704984,-73.99009296,"E 17 St & Broadway"
245 | 498,40.74854862,-73.98808416,"Broadway & W 32 St"
246 | 499,40.76915505,-73.98191841,"Broadway & W 60 St"
247 | 500,40.76228826,-73.98336183,"Broadway & W 51 St"
248 | 501,40.744219,-73.97121214,"FDR Drive & E 35 St"
249 | 502,40.714215,-73.981346,"Henry St & Grand St"
250 | 503,40.73827428,-73.98751968,"E 20 St & Park Ave"
251 | 504,40.73221853,-73.98165557,"1 Ave & E 16 St"
252 | 505,40.74901271,-73.98848395,"6 Ave & W 33 St"
253 | 507,40.73912601,-73.97973776,"E 25 St & 2 Ave"
254 | 508,40.76341379,-73.99667444,"W 46 St & 11 Ave"
255 | 509,40.7454973,-74.00197139,"9 Ave & W 22 St"
256 | 513,40.768254,-73.988639,"W 56 St & 10 Ave"
257 | 514,40.76087502,-74.00277668,"12 Ave & W 40 St"
258 | 515,40.76009437,-73.99461843,"W 43 St & 10 Ave"
259 | 516,40.75206862,-73.96784384,"E 47 St & 1 Ave"
260 | 517,40.751581,-73.97791,"Pershing Square South"
261 | 518,40.74780373,-73.9734419,"E 39 St & 2 Ave"
262 | 519,40.751873,-73.977706,"Pershing Square North"
263 | 520,40.75992262,-73.97648516,"W 52 St & 5 Ave"
264 | 522,40.75714758,-73.97207836,"E 51 St & Lexington Ave"
265 | 523,40.75466591,-73.99138152,"W 38 St & 8 Ave"
266 | 524,40.75527307,-73.98316936,"W 43 St & 6 Ave"
267 | 525,40.75594159,-74.0021163,"W 34 St & 11 Ave"
268 | 526,40.74765947,-73.98490707,"E 33 St & 5 Ave"
269 | 527,40.744023,-73.976056,"E 33 St & 2 Ave"
270 | 528,40.74290902,-73.97706058,"2 Ave & E 31 St"
271 | 529,40.7575699,-73.99098507,"W 42 St & 8 Ave"
272 | 530,40.771522,-73.990541,"11 Ave & W 59 St"
273 | 531,40.71893904,-73.99266288,"Forsyth St & Broome St"
274 | 532,40.710451,-73.960876,"S 5 Pl & S 4 St"
275 | 534,40.70255065,-74.0127234,"Water - Whitehall Plaza"
276 | 536,40.74144387,-73.97536082,"1 Ave & E 30 St"
277 | 537,40.74025878,-73.98409214,"Lexington Ave & E 24 St"
278 | 539,40.71534825,-73.96024116,"Metropolitan Ave & Bedford Ave"
279 | 540,40.74311555376486,-73.98215353488922,"Lexington Ave & E 29 St"
280 | 545,40.736502,-73.97809472,"E 23 St & 1 Ave"
281 | 546,40.74444921,-73.98303529,"E 30 St & Park Ave S"
282 | 2000,40.70255088,-73.98940236,"Front St & Washington St"
283 | 2001,40.699773,-73.979927,"Sands St & Navy St"
284 | 2002,40.716887,-73.963198,"Wythe Ave & Metropolitan Ave"
285 | 2003,40.733812191966315,-73.98054420948029,"1 Ave & E 18 St"
286 | 2005,40.70531194,-73.97100056,"Railroad Ave & Kay Ave"
287 | 2006,40.76590936,-73.97634151,"Central Park S & 6 Ave"
288 | 2008,40.70569254,-74.01677685,"Little West St & 1 Pl"
289 | 2009,40.71117444,-73.99682619,"Catherine St & Monroe St"
290 | 2010,40.72165481,-74.00234737,"Grand St & Greene St"
291 | 2012,40.739445,-73.976806,"E 27 St & 1 Ave"
292 | 2021,40.75929124,-73.98859651,"W 45 St & 8 Ave"
293 | 2022,40.759107,-73.959223,"E 60 St & York Ave"
294 | 2023,40.75968085,-73.97031366,"E 55 St & Lexington Ave"
295 | 3002,40.711512,-74.015756,"South End Ave & Liberty St"
296 | 3016,40.72036775298455,-73.96165072917938,"Kent Ave & N 7 St"
297 | 3041,40.67890679,-73.94142771,"Kingston Ave & Herkimer St"
298 | 3042,40.6794268,-73.9298911,"Fulton St & Utica Ave"
299 | 3043,40.6814598,-73.934903,"Lewis Ave & Decatur St"
300 | 3044,40.6800105,-73.938475,"Albany Ave & Fulton St"
301 | 3046,40.682601,-73.938037,"Marcus Garvey Blvd & Macon St"
302 | 3047,40.6823687,-73.944118,"Halsey St & Tompkins Ave"
303 | 3048,40.68402,-73.94977,"Putnam Ave & Nostrand Ave"
304 | 3049,40.68488,-73.96304,"Cambridge Pl & Gates Ave"
305 | 3050,40.6851532,-73.94111,"Putnam Ave & Throop Ave"
306 | 3052,40.686312,-73.935775,"Lewis Ave & Madison St"
307 | 3053,40.6900815,-73.947915,"Marcy Ave & Lafayette Ave"
308 | 3054,40.6894932,-73.942061,"Greene Ave & Throop Ave"
309 | 3055,40.6883337,-73.950916,"Greene Ave & Nostrand Ave"
310 | 3056,40.69072549,-73.95133465,"Kosciuszko St & Nostrand Ave"
311 | 3057,40.69128258,-73.9452416,"Kosciuszko St & Tompkins Ave"
312 | 3058,40.69237074,-73.93705428,"Lewis Ave & Kosciuszko St"
313 | 3059,40.6933982,-73.939877,"Pulaski St & Marcus Garvey Blvd"
314 | 3060,40.69425403,-73.94626915,"Willoughby Ave & Tompkins Ave"
315 | 3061,40.69622937,-73.94371094,"Throop Ave & Myrtle Ave"
316 | 3062,40.69539817,-73.94954908,"Myrtle Ave & Marcy Ave"
317 | 3063,40.69527008,-73.95238108,"Nostrand Ave & Myrtle Ave"
318 | 3064,40.69681963,-73.93756926,"Myrtle Ave & Lewis Ave"
319 | 3065,40.70029511,-73.95032283,"Union Ave & Wallabout St"
320 | 3066,40.69957608,-73.94708417,"Tompkins Ave & Hopkins St"
321 | 3067,40.7016657,-73.9437303,"Broadway & Whipple St"
322 | 3068,40.7031724,-73.940636,"Humboldt St & Varet St"
323 | 3069,40.70411791,-73.94818595,"Lorimer St & Broadway"
324 | 3070,40.70510918,-73.94407279,"McKibbin St & Manhattan Ave"
325 | 3071,40.70538077,-73.94976519,"Boerum St & Broadway"
326 | 3072,40.70583339,-73.94644578,"Leonard St & Boerum St"
327 | 3073,40.70691254,-73.95441667,"Division Ave & Hooper St"
328 | 3074,40.70767788,-73.94016171,"Montrose Ave & Bushwick Ave"
329 | 3075,40.70708701,-73.95796783,"Division Ave & Marcy Ave"
330 | 3076,40.70870368,-73.9448625,"Scholes St & Manhattan Ave"
331 | 3077,40.70877084,-73.95095259,"Stagg St & Union Ave"
332 | 3078,40.70924826276157,-73.96063148975372,"Broadway & Roebling St"
333 | 3080,40.70934,-73.95608,"S 4 St & Rodney St"
334 | 3081,40.711863,-73.944024,"Graham Ave & Grand St"
335 | 3082,40.71167351,-73.95141312,"Hope St & Union Ave"
336 | 3083,40.71247661,-73.94100005,"Bushwick Ave & Powers St"
337 | 3085,40.71469,-73.95739,"Roebling St & N 4 St"
338 | 3086,40.715143,-73.944507,"Graham Ave & Conselyea St"
339 | 3087,40.71413311,-73.95234386,"Metropolitan Ave & Meeker Ave"
340 | 3088,40.7160751,-73.952029,"Union Ave & Jackson St"
341 | 3090,40.71774592,-73.95600096,"N 8 St & Driggs Ave"
342 | 3091,40.71764,-73.94882,"Frost St & Meeker St"
343 | 3092,40.7190095,-73.95852515,"Berry St & N 8 St"
344 | 3093,40.71745169,-73.95850939,"N 6 St & Bedford Ave"
345 | 3094,40.7169811,-73.94485918,"Graham Ave & Withers St"
346 | 3095,40.71929301,-73.94500379,"Graham Ave & Herbert St"
347 | 3096,40.71924,-73.95242,"Union Ave & N 12 St"
348 | 3100,40.724812564400175,-73.94752621650696,"Nassau Ave & Newell St"
349 | 3101,40.72079821,-73.95484712,"N 12 St & Bedford Ave"
350 | 3102,40.72179134,-73.9504154,"Driggs Ave & Lorimer St"
351 | 3103,40.72153267,-73.95782357,"N 11 St & Wythe Ave"
352 | 3105,40.724055,-73.955736,"N 15 St & Wythe Ave"
353 | 3106,40.72325,-73.94308,"Driggs Ave & N Henry St"
354 | 3107,40.72311651,-73.95212324,"Bedford Ave & Nassau Ave"
355 | 3108,40.72557,-73.94434,"Nassau Ave & Russell St"
356 | 3109,40.72606,-73.95621,"Banker St & Meserole Ave"
357 | 3110,40.72708584,-73.95299117,"Meserole Ave & Manhattan Ave"
358 | 3112,40.72906,-73.95779,"Milton St & Franklin St"
359 | 3113,40.73026,-73.95394,"Greenpoint Ave & Manhattan Ave"
360 | 3115,40.73232194,-73.9550858,"India St & Manhattan Ave"
361 | 3116,40.73266,-73.95826,"Huron St & Franklin St"
362 | 3117,40.73564,-73.95866,"Franklin St & Dupont St"
363 | 3118,40.73555,-73.95284,"McGuinness Blvd & Eagle St"
364 | 3119,40.74232744,-73.95411749,"Vernon Blvd & 50 Ave"
365 | 3120,40.74161,-73.96044,"Center Blvd & Borden Ave"
366 | 3121,40.74524768,-73.94733276,"Jackson Ave & 46 Rd"
367 | 3122,40.744363287066875,-73.9558732509613,"48 Ave & 5 St"
368 | 3123,40.74469738,-73.93540375,"31 St & Thomson Ave"
369 | 3124,40.74731,-73.95451,"46 Ave & 5 St"
370 | 3125,40.74708586,-73.94977234,"45 Rd & 11 St"
371 | 3126,40.74718234,-73.9432635,"44 Dr & Jackson Ave"
372 | 3127,40.74966,-73.9521,"9 St & 44 Rd"
373 | 3128,40.75052534,-73.94594845,"21 St & 43 Ave"
374 | 3129,40.75110165,-73.94073717,"Queens Plaza North & Crescent St"
375 | 3130,40.75325964,-73.94335788,"21 St & Queens Plaza North"
376 | 3131,40.76712840349542,-73.96224617958069,"E 68 St & 3 Ave"
377 | 3132,40.76350532,-73.97109243,"E 59 St & Madison Ave"
378 | 3134,40.76312584,-73.96526895,"3 Ave & E 62 St"
379 | 3135,40.77112927,-73.95772297,"E 75 St & 3 Ave"
380 | 3136,40.766368,-73.971518,"5 Ave & E 63 St"
381 | 3137,40.77282817,-73.96685276,"5 Ave & E 73 St"
382 | 3139,40.77118287540658,-73.96409422159195,"E 72 St & Park Ave"
383 | 3140,40.77140426,-73.9535166,"1 Ave & E 78 St"
384 | 3141,40.76500525,-73.95818491,"1 Ave & E 68 St"
385 | 3142,40.7612274,-73.96094022,"1 Ave & E 62 St"
386 | 3143,40.77632142182271,-73.96427392959595,"5 Ave & E 78 St"
387 | 3144,40.77677702,-73.9590097,"E 81 St & Park Ave"
388 | 3145,40.77862688,-73.95772073,"E 84 St & Park Ave"
389 | 3146,40.77573034,-73.9567526,"E 81 St & 3 Ave"
390 | 3147,40.77801203,-73.95407149,"E 85 St & 3 Ave"
391 | 3148,40.77565541,-73.95068615,"E 84 St & 1 Ave"
392 | 3150,40.77536905,-73.94803392,"E 85 St & York Ave"
393 | 3151,40.7728384,-73.94989233,"E 81 St & York Ave"
394 | 3152,40.76873687,-73.96119945,"3 Ave & E 71 St"
395 | 3155,40.76440023,-73.96648977,"Lexington Ave & E 63 St"
396 | 3156,40.76663814,-73.95348296,"E 72 St & York Ave"
397 | 3157,40.77518615,-73.94446054,"East End Ave & E 86 St"
398 | 3158,40.77163851,-73.98261428,"W 63 St & Broadway"
399 | 3159,40.77492513,-73.98266566,"W 67 St & Broadway"
400 | 3160,40.77896784,-73.97374737,"Central Park West & W 76 St"
401 | 3161,40.7801839724239,-73.97728532552719,"W 76 St & Columbus Ave"
402 | 3162,40.78339981,-73.98093133,"W 78 St & Broadway"
403 | 3163,40.7734066,-73.97782542,"Central Park West & W 68 St"
404 | 3164,40.7770575,-73.97898475,"Columbus Ave & W 72 St"
405 | 3165,40.77579376683666,-73.9762057363987,"Central Park West & W 72 St"
406 | 3166,40.78057799010334,-73.98562431335449,"Riverside Dr & W 72 St"
407 | 3167,40.77966809007312,-73.98093044757842,"Amsterdam Ave & W 73 St"
408 | 3168,40.78472675,-73.96961715,"Central Park West & W 85 St"
409 | 3169,40.78720869,-73.98128127,"Riverside Dr & W 82 St"
410 | 3170,40.78499979,-73.97283406,"W 84 St & Columbus Ave"
411 | 3171,40.78524672,-73.97667321,"Amsterdam Ave & W 82 St"
412 | 3172,40.7785669,-73.97754961,"W 74 St & Columbus Ave"
413 | 3173,40.777507027547976,-73.98888587951659,"Riverside Blvd & W 67 St"
414 | 3175,40.77748046,-73.98288594,"W 70 St & Amsterdam Ave"
415 | 3176,40.77452835,-73.98753759,"W 64 St & West End Ave"
416 | 3177,40.7867947,-73.977112,"W 84 St & Broadway"
417 | 3178,40.78414472,-73.98362492,"Riverside Dr & W 78 St"
418 | 3179,40.698617,-73.941342,"Park Ave & Marcus Garvey Blvd"
419 | 3180,40.69878,-73.99712,"Brooklyn Bridge Park - Pier 2"
420 | 3182,40.686931,-74.016966,"Yankee Ferry Terminal"
421 | 3221,40.743,-73.93561,"47 Ave & 31 St"
422 | 3223,40.758996559605116,-73.96865397691727,"E 55 St & 3 Ave"
423 | 3224,40.73997354103409,-74.00513872504234,"W 13 St & Hudson St"
424 | 3226,40.78275,-73.97137,"W 82 St & Central Park West"
425 | 3231,40.76780080148132,-73.96592080593109,"E 67 St & Park Ave"
426 | 3232,40.68962188790333,-73.98304268717766,"Bond St & Fulton St"
427 | 3233,40.75724567911726,-73.97805914282799,"E 48 St & 5 Ave"
428 | 3235,40.752165280621966,-73.97992193698882,"E 41 St & Madison Ave"
429 | 3236,40.75898481399634,-73.99379968643188,"W 42 St & Dyer Ave"
430 | 3238,40.77391390238118,-73.9543953537941,"E 80 St & 2 Ave"
431 | 3241,40.686203,-73.944694,"Monroe St & Tompkins Ave"
432 | 3242,40.69102925677968,-73.99183362722397,"Schermerhorn St & Court St"
433 | 3243,40.75892386377695,-73.96226227283478,"E 58 St & 1 Ave"
434 | 3244,40.73143724085228,-73.99490341544151,"University Pl & E 8 St"
435 | 3245,40.65594917783975,-74.00835871696472,"Industry City, Building 1 Basement"
436 | 3249,40.6803560840434,-73.9476791024208,"Verona Pl & Fulton St"
437 | 3250,40.71690978045965,-73.98383796215057,"NYCBS Depot - PIT"
438 | 3254,40.69231660719192,-74.01486575603485,"Soissons Landing"
439 | 3255,40.7505853470215,-73.9946848154068,"8 Ave & W 31 St"
440 | 3256,40.7277140777778,-74.01129573583603,"Pier 40 - Hudson River Park"
441 | 3258,40.75018156325683,-74.00218427181244,"W 27 St & 10 Ave"
442 | 3259,40.74937024193277,-73.99923384189606,"9 Ave & W 28 St"
443 | 3260,40.72706363348306,-73.99662137031554,"Mercer St & Bleecker St"
444 | 3263,40.72923649910006,-73.99086803197861,"Cooper Square & E 7 St"
445 | 3276,40.71458403535893,-74.04281705617905,"Marin Light Rail"
446 | 3282,40.78307,-73.95939,"5 Ave & E 88 St"
447 | 3283,40.7882213,-73.97041561,"W 89 St & Columbus Ave"
448 | 3284,40.781410700190015,-73.95595908164978,"E 88 St & Park Ave"
449 | 3285,40.78839,-73.9747,"W 87 St & Amsterdam Ave"
450 | 3286,40.7806284,-73.9521667,"E 89 St & 3 Ave"
451 | 3288,40.778301,-73.9488134,"E 88 St & 1 Ave"
452 | 3289,40.79017948095081,-73.97288918495178,"W 90 St & Amsterdam Ave"
453 | 3290,40.7779453,-73.946041,"E 89 St & York Ave"
454 | 3292,40.7857851,-73.957481,"5 Ave & E 93 St"
455 | 3293,40.7921,-73.9739,"W 92 St & Broadway"
456 | 3294,40.7835016,-73.955327,"E 91 St & Park Ave"
457 | 3295,40.79127,-73.964839,"Central Park W & W 96 St"
458 | 3297,40.6686627,-73.97988067,"6 St & 7 Ave"
459 | 3298,40.686371,-73.99383324,"Warren St & Court St"
460 | 3300,40.66514681533792,-73.97637605667114,"Prospect Park West & 8 St"
461 | 3301,40.7919557,-73.968087,"Columbus Ave & W 95 St"
462 | 3302,40.7969347,-73.9643412291,"Columbus Ave & W 103 St"
463 | 3303,40.6849894,-73.99440329,"Butler St & Court St"
464 | 3304,40.668127,-73.98377641,"6 Ave & 9 St"
465 | 3305,40.78112229934166,-73.94965589046478,"E 91 St & 2 Ave"
466 | 3306,40.6662078,-73.98199886,"10 St & 7 Ave"
467 | 3307,40.7941654,-73.974124,"West End Ave & W 94 St"
468 | 3308,40.6861758,-73.99645295,"Kane St & Clinton St"
469 | 3309,40.7859201,-73.94860294,"E 97 St & 3 Ave"
470 | 3310,40.663779,-73.98396846,"14 St & 7 Ave"
471 | 3311,40.68763155,-74.0016256,"Columbia St & Kane St"
472 | 3312,40.7817212,-73.94594,"1 Ave & E 94 St"
473 | 3313,40.6663181,-73.9854617,"6 Ave & 12 St"
474 | 3314,40.7937704,-73.971888,"W 95 St & Broadway"
475 | 3315,40.6847514,-73.99917254,"Henry St & Degraw St"
476 | 3316,40.7989937,-73.9662173778,"W 104 St & Amsterdam Ave"
477 | 3317,40.6686273,-73.98700053,"10 St & 5 Ave"
478 | 3318,40.7839636,-73.9471673,"2 Ave & E 96 St"
479 | 3319,40.666287,-73.98895053,"14 St & 5 Ave"
480 | 3320,40.793393,-73.9635556,"Central Park West & W 100 St"
481 | 3321,40.6831164,-73.99785267,"Clinton St & Union St"
482 | 3322,40.668603,-73.9904394,"12 St & 4 Ave"
483 | 3323,40.7981856,-73.9605909006,"W 106 St & Central Park West"
484 | 3324,40.6685455,-73.99333264,"3 Ave & 14 St"
485 | 3325,40.7849032,-73.950503,"E 95 St & 3 Ave"
486 | 3326,40.67434,-74.00194698,"Clinton St & Centre St"
487 | 3327,40.7877214,-73.94728331,"3 Ave & E 100 St"
488 | 3328,40.795,-73.9645,"W 100 St & Manhattan Ave"
489 | 3329,40.6829151,-73.99318208,"Degraw St & Smith St"
490 | 3330,40.6725058,-74.00494695,"Henry St & Bay St"
491 | 3331,40.8013434,-73.9711457439,"Riverside Dr & W 104 St"
492 | 3332,40.681990442707026,-73.99079024791718,"Degraw St & Hoyt St"
493 | 3333,40.6747055,-74.0075572,"Columbia St & Lorraine St"
494 | 3335,40.6772744,-73.98282002,"Union St & 4 Ave"
495 | 3336,40.787801,-73.953559,"E 97 St & Madison Ave"
496 | 3337,40.67363551341504,-74.01195555925369,"Dwight St & Van Dyke St"
497 | 3338,40.7862586,-73.94552579,"2 Ave & E 99 St"
498 | 3339,40.6765304,-73.97846879,"Berkeley Pl & 6 Ave"
499 | 3340,40.6753274,-74.0100698,"Wolcott St & Dwight St"
500 | 3341,40.795346,-73.96186,"Central Park West & W 102 St"
501 | 3342,40.6777748,-74.0094613,"Pioneer St & Richards St"
502 | 3343,40.7997568,-73.9621128676,"W 107 St & Columbus Ave"
503 | 3344,40.679043,-74.011169,"Pioneer St & Van Brunt St"
504 | 3345,40.78948541553215,-73.95242929458618,"Madison Ave & E 99 St"
505 | 3346,40.675146838709786,-73.97523209452629,"Berkeley Pl & 7 Ave"
506 | 3347,40.6773429,-74.01275056,"Van Brunt St & Wolcott St"
507 | 3348,40.677236,-74.015665,"Coffey St & Conover St"
508 | 3349,40.6729679,-73.97087984,"Grand Army Plaza & Plaza St West"
509 | 3350,40.7973721,-73.97041192,"W 100 St & Broadway"
510 | 3351,40.7869946,-73.94164802,"E 102 St & 1 Ave"
511 | 3352,40.67267243410948,-74.0087952464819,"Sigourney St & Columbia St"
512 | 3353,40.6747844,-74.01612847,"Reed St & Van Brunt St"
513 | 3354,40.668132,-73.97363831,"3 St & Prospect Park West"
514 | 3355,40.76800889305947,-73.96845281124115,"E 66 St & Madison Ave"
515 | 3356,40.7746671,-73.98470567,"Amsterdam Ave & W 66 St"
516 | 3357,40.8008363,-73.9664492472,"W 106 St & Amsterdam Ave"
517 | 3358,40.6711978,-73.97484126,"Garfield Pl & 8 Ave"
518 | 3359,40.7691572,-73.96703464,"E 68 St & Madison Ave"
519 | 3360,40.7829391,-73.9786517,"Amsterdam Ave & W 79 St"
520 | 3361,40.6740886,-73.9787282,"Carroll St & 6 Ave"
521 | 3362,40.7781314,-73.96069399,"Madison Ave & E 82 St"
522 | 3363,40.7904828,-73.95033068,"E 102 St & Park Ave"
523 | 3364,40.6751622,-73.9814832,"Carroll St & 5 Ave"
524 | 3365,40.6703837,-73.97839676,"3 St & 7 Ave"
525 | 3366,40.8021174,-73.9681805305,"West End Ave & W 107 St"
526 | 3367,40.7922553,-73.95249933,"5 Ave & E 103 St"
527 | 3368,40.6728155,-73.98352355,"5 Ave & 3 St"
528 | 3369,40.772460677893974,-73.94682079553604,"E 82 St & East End Ave"
529 | 3370,40.7727966,-73.95577801,"E 78 St & 2 Ave"
530 | 3371,40.674613422475815,-73.98501142859459,"4 Ave & 2 St"
531 | 3372,40.7689738,-73.95482273,"E 74 St & 1 Ave"
532 | 3373,40.6750705,-73.98775226,"3 St & 3 Ave"
533 | 3374,40.799484,-73.955613,"Central Park North & Adam Clayton Powell Blvd"
534 | 3375,40.7699426,-73.96060712,"3 Ave & E 72 St"
535 | 3376,40.76471851944339,-73.96222069859505,"E 65 St & 2 Ave"
536 | 3377,40.6786115,-73.99037292,"Carroll St & Bond St"
537 | 3378,40.773763,-73.96222088,"E 76 St & Park Ave"
538 | 3379,40.7903051,-73.94755757,"E 103 St & Lexington Ave"
539 | 3381,40.6777287,-73.99364123,"3 St & Hoyt St"
540 | 3382,40.680611,-73.99475825,"Carroll St & Smith St"
541 | 3383,40.804213,-73.96699104,"Cathedral Pkwy & Broadway"
542 | 3384,40.6787242,-73.99599099,"Smith St & 3 St"
543 | 3386,40.6809591,-73.99905709,"1 Pl & Clinton St"
544 | 3387,40.7934337,-73.94945003,"E 106 St & Madison Ave"
545 | 3388,40.6828003,-73.99990419,"President St & Henry St"
546 | 3389,40.6830456,-74.00348559,"Carroll St & Columbia St"
547 | 3390,40.79329668,-73.9432083,"E 109 St & 3 Ave"
548 | 3391,40.7892529,-73.93956237,"E 106 St & 1 Ave"
549 | 3392,40.6812117,-74.00860912,"Commerce St & Van Brunt St"
550 | 3393,40.6794327,-74.00785041,"Richards St & Delavan St"
551 | 3394,40.6769993,-74.00647134,"Columbia St & W 9 St"
552 | 3395,40.6763744,-74.00324957,"Henry St & W 9 St"
553 | 3396,40.6783563,-74.00014502,"Clinton St & 4 Place"
554 | 3397,40.6763947,-73.99869893,"Court St & Nelson St"
555 | 3398,40.6746957,-73.99785768,"Smith St & 9 St"
556 | 3399,40.67260298150126,-73.98983001708984,"7 St & 3 Ave"
557 | 3400,40.7961535,-73.94782145,"E 110 St & Madison Ave"
558 | 3401,40.6724811,-73.99331394,"2 Ave & 9 St"
559 | 3402,40.6902375,-73.99203074,"Court St & State St"
560 | 3403,40.6705135,-73.98876585,"4 Ave & 9 St"
561 | 3404,40.6704922,-73.98541675,"7 St & 5 Ave"
562 | 3405,40.6704836,-73.98208968,"5 St & 6 Ave"
563 | 3407,40.67909799721684,-73.98765474557877,"Union St & Nevins St"
564 | 3408,40.6881529,-73.99520919,"Congress St & Clinton St"
565 | 3409,40.6867443,-73.99063168,"Bergen St & Smith St"
566 | 3410,40.6864442,-73.98759104,"Dean St & Hoyt St"
567 | 3411,40.6849668,-73.98620772,"Bond St & Bergen St"
568 | 3412,40.6853761,-73.98302136,"Pacific St & Nevins St"
569 | 3413,40.6827549,-73.98258555,"Wyckoff St & 3 Ave"
570 | 3414,40.680944723477296,-73.97567331790923,"Bergen St & Flatbush Ave"
571 | 3415,40.6793307,-73.97519523,"Prospect Pl & 6 Ave"
572 | 3416,40.6776147,-73.97324283,"7 Ave & Park Pl"
573 | 3417,40.6795766,-73.97854971,"Baltic St & 5 Ave"
574 | 3418,40.6750207,-73.97111473,"Plaza St West & Flatbush Ave"
575 | 3419,40.6792788,-73.98154004,"Douglass St & 4 Ave"
576 | 3420,40.6802133,-73.98432695,"Douglass St & 3 Ave"
577 | 3421,40.6843549,-73.98901629,"Hoyt St & Warren St"
578 | 3422,40.6859296,-74.00242364,"Columbia St & Degraw St"
579 | 3423,40.6610633719006,-73.97945255041122,"West Drive & Prospect Park West"
580 | 3424,40.791976,-73.945993,"E 106 St & Lexington Ave"
581 | 3425,40.7892105,-73.94370784,"2 Ave & E 104 St"
582 | 3427,40.72430527250332,-73.99600982666016,"Lafayette St & Jersey St"
583 | 3428,40.740983,-74.001702,"8 Ave & W 16 St"
584 | 3429,40.68506807308177,-73.97790759801863,"Hanson Pl & Ashland Pl"
585 | 3430,40.71907891179564,-73.94223690032959,"Richardson St & N Henry St"
586 | 3431,40.746524,-73.977885,"E 35 St & 3 Ave"
587 | 3432,40.66906013501107,-73.99463653564453,"NYCBS Depot - GOW"
588 | 3434,40.79025417330419,-73.97718340158461,"W 88 St & West End Ave"
589 | 3435,40.718822,-73.99596,"Grand St & Elizabeth St"
590 | 3436,40.721319,-74.010065,"Greenwich St & Hubert St"
591 | 3438,40.772248537721744,-73.95842134952545,"E 76 St & 3 Ave"
592 | 3440,40.692418292578466,-73.98949474096298,"Fulton St & Adams St"
593 | 3441,40.752957,-74.00264,"10 Hudson Yards"
594 | 3443,40.76132983124814,-73.97982001304626,"W 52 St & 6 Ave"
595 | 3445,40.79181171501097,-73.9786022901535,"Riverside Dr & W 89 St"
596 | 3447,40.76703432309872,-73.95622730255127,"E 71 St & 1 Ave"
597 | 3449,40.721462562298164,-73.94800901412964,"Eckford St & Engert Ave"
598 | 3452,40.71915571696044,-73.94885390996933,"Bayard St & Leonard St"
599 | 3453,40.71335226222875,-73.9491033554077,"Devoe St & Lorimer St"
600 | 3454,40.7103685423523,-73.94705951213837,"Leonard St & Maujer St"
601 | 3455,40.68680820503432,-73.98036181926727,"Schermerhorn St & 3 Ave"
602 | 3456,40.71638031973561,-73.94821286201477,"Jackson St & Leonard St"
603 | 3457,40.76302594280519,-73.97209525108337,"E 58 St & Madison Ave"
604 | 3458,40.76309387270797,-73.9783501625061,"W 55 St & 6 Ave"
605 | 3459,40.75763227739443,-73.96930575370789,"E 53 St & 3 Ave"
606 | 3461,40.714851505262516,-74.01122331619263,"Murray St & Greenwich St"
607 | 3462,40.75118387463277,-73.97138714790344,"E 44 St & 2 Ave"
608 | 3463,40.735367055605394,-73.98797392845154,"E 16 St & Irving Pl"
609 | 3464,40.75227093837409,-73.98770570755005,"W 37 St & Broadway"
610 | 3466,40.75668720603179,-73.98257732391357,"W 45 St & 6 Ave"
611 | 3468,40.7303801,-73.9747502,"NYCBS Depot - STY - Garage 4"
612 | 3469,40.73181401720966,-73.95995020866394,"India St & West St"
613 | 3472,40.742753828659026,-74.00747358798981,"W 15 St & 10 Ave"
614 | 3474,40.7252556952547,-74.00412082672119,"6 Ave & Spring St"
615 | 3476,40.72576996727214,-73.9507395029068,"Norman Ave & Leonard St"
616 |
--------------------------------------------------------------------------------
/images/citibike-trips.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rolyatmax/citibike-trips/89a46dadbbff0f5737316cc1a0ac55296347fbd9/images/citibike-trips.png
--------------------------------------------------------------------------------
/images/citibike-trips.sketch:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rolyatmax/citibike-trips/89a46dadbbff0f5737316cc1a0ac55296347fbd9/images/citibike-trips.sketch
--------------------------------------------------------------------------------
/images/citibike.mov:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rolyatmax/citibike-trips/89a46dadbbff0f5737316cc1a0ac55296347fbd9/images/citibike.mov
--------------------------------------------------------------------------------
/images/design.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rolyatmax/citibike-trips/89a46dadbbff0f5737316cc1a0ac55296347fbd9/images/design.png
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | citi bike commute
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
150 |
151 |
152 | Loading a couple megabytes of data. This should only take a few seconds. If you're on a slow connection, this might take a little longer.
153 |
154 |
155 |
156 |
157 |
The Citi Bike Commute
158 |
159 |
Hold shift and point your mouse to a part of the map to show trips originating in that area. Click, double click, drag, and scroll to change your vantage point. Finally, click the timeline to jump to a different part of the day.
160 |
161 |
10:35 AM
162 |
Monday
163 |
17 July 2017
164 |
165 |
166 |
167 |
168 |
169 |
170 |
180 |
181 |
182 |
--------------------------------------------------------------------------------
/old-code/main.py:
--------------------------------------------------------------------------------
1 | # main.py
2 | import csv
3 |
4 | import numpy
5 | from Pycluster import *
6 |
7 | data_dir = 'data/'
8 | paths = ['2013-07.csv', '2013-08.csv', '2013-09.csv', '2013-10.csv',
9 | '2013-11.csv', '2013-12.csv', '2014-01.csv', '2014-02.csv']
10 |
11 | counts = []
12 | headers = []
13 | genders = {}
14 | usertypes = {}
15 | birthyears = {}
16 | rides = {}
17 | routes = {}
18 | stations = {}
19 |
20 | def crunch_numbers():
21 | most_traveled = 0
22 | most_traveled_alert = 'I got nothin'
23 |
24 | for i in range(len(paths)):
25 | ifile = open(data_dir + paths[i], 'rb')
26 | reader = csv.reader(ifile)
27 | counts.append(0)
28 | for row in reader:
29 | if counts[i] == 0:
30 | headers.append(row)
31 | else:
32 | gender = row[14]
33 | usertype = row[12]
34 | birthyear = row[13]
35 | bikeid = row[11]
36 | start_station = row[3]
37 | start_station_name = row[4]
38 | start_station_lat = row[5]
39 | start_station_long = row[6]
40 | end_station = row[7]
41 | end_station_name = row[8]
42 | end_station_lat = row[9]
43 | end_station_long = row[10]
44 |
45 | genders.setdefault(gender, 0)
46 | usertypes.setdefault(usertype, 0)
47 | birthyears.setdefault(birthyear, 0)
48 | rides.setdefault(bikeid, 0)
49 | routes.setdefault(start_station, {})
50 | routes[start_station].setdefault(end_station, 0)
51 |
52 | station_info = {
53 | 'id': start_station,
54 | 'name': start_station_name,
55 | 'lat': start_station_lat,
56 | 'long': start_station_long
57 | }
58 |
59 | stations.setdefault(start_station, station_info)
60 |
61 | if start_station != end_station:
62 | station_info = {
63 | 'id': end_station,
64 | 'name': end_station_name,
65 | 'lat': end_station_lat,
66 | 'long': end_station_long
67 | }
68 | stations.setdefault(end_station, station_info)
69 |
70 | genders[gender] += 1
71 | usertypes[usertype] += 1
72 | birthyears[birthyear] += 1
73 | rides[bikeid] += 1
74 | routes[start_station][end_station] += 1
75 |
76 | trips = routes[start_station][end_station]
77 | if trips > most_traveled:
78 | most_traveled = trips
79 | most_traveled_alert = '%s -> %s : %d trips' % (start_station_name, end_station_name, trips)
80 |
81 | counts[i] += 1
82 |
83 | print('%d percent done' % (float(i + 1) / len(paths) * 100))
84 |
85 | # just check and make sure all the headers are the same
86 | for j in range(len(headers)):
87 | if j == 0: continue
88 | if headers[j] != headers[j - 1]:
89 | print("%s does not equal %s" % (j, j - 1))
90 |
91 | years = birthyears.keys()
92 | years.sort()
93 |
94 | print('Creating station file data')
95 | f = open(data_dir + 'station_data.csv', 'wb')
96 | writer = csv.writer(f, delimiter='\t')
97 | [writer.writerow(s.values()) for s in stations.values()]
98 |
99 | print('\n')
100 | print('Header: %r' % headers[0])
101 | print('\n')
102 | print('Count: %r' % sum(counts))
103 | print('Genders: %r' % genders)
104 | print('User Types: %r' % usertypes)
105 | print('Average number of rides per bike: %d' % (sum(rides.values()) / len(rides)))
106 | print('Station Count: %d' % len(stations))
107 | print(most_traveled_alert)
108 | print('\n')
109 |
110 | crunch_numbers()
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "citibike-trips",
3 | "version": "0.0.2",
4 | "scripts": {
5 | "start": "budo src/index.js:bundle.js --live --open",
6 | "build": "browserify src/index.js > bundle.js",
7 | "serve": "open http://localhost:8080 && python -m SimpleHTTPServer 8080",
8 | "lint": "standard"
9 | },
10 | "dependencies": {
11 | "3d-view-controls": "^2.2.0",
12 | "argv": "^0.0.2",
13 | "browserify": "^14.4.0",
14 | "budo": "^10.0.3",
15 | "camera-picking-ray": "^1.0.1",
16 | "canvas-fit": "^1.5.0",
17 | "cat-rom-spline": "^1.0.0",
18 | "color": "^2.0.0",
19 | "d3-geo": "^1.6.4",
20 | "dat-gui": "^0.5.0",
21 | "dom-css": "^2.1.0",
22 | "eslint": "^4.2.0",
23 | "fast-csv": "^2.4.0",
24 | "geojson-bbox": "^0.0.0",
25 | "gl-mat4": "^1.1.4",
26 | "gl-vec2": "^1.0.0",
27 | "glslify": "^6.1.0",
28 | "lerp": "^1.0.3",
29 | "new-array": "^1.0.0",
30 | "projections": "^1.0.0",
31 | "ray-plane-intersection": "^1.0.0",
32 | "regl": "^1.3.0",
33 | "smoothstep": "^1.0.1",
34 | "spring-animator": "^1.0.3",
35 | "standard": "^10.0.2",
36 | "topojson": "^3.0.0"
37 | },
38 | "browserify": {
39 | "transform": [
40 | "glslify"
41 | ]
42 | },
43 | "eslintConfig": {
44 | "extends": [
45 | "standard",
46 | "standard-jsx"
47 | ]
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/scripts/clean-citibike-trips.js:
--------------------------------------------------------------------------------
1 | // go through all citibike trip csv data and output two files:
2 | // 1. stations.csv - station_id,lat,lon,name
3 | // 2. trips.csv - start_ts,duration,start_station,end_station,subscriber,birth_year
4 |
5 | const fs = require('fs')
6 | const csv = require('fast-csv')
7 |
8 | const STATIONS_OUT = './stations.csv'
9 | const stationsHeader = 'station_id,latitude,longitude,name'
10 | const stations = {}
11 |
12 | process.stdout.write('start_ts,duration,start_station,end_station,subscriber,birth_year\n')
13 |
14 | csv
15 | .fromStream(process.stdin, { headers: true })
16 | .on('data', processLine)
17 | .on('end', onEnd)
18 |
19 | function processLine (data) {
20 | const startTs = data['starttime']
21 | const duration = int(data['tripduration'])
22 | const startStation = int(data['start station id'])
23 | const endStation = int(data['end station id'])
24 | const subscriber = data['usertype'] === 'Subscriber' ? 1 : 0
25 | const birthYear = int(data['birth year']) || 0
26 |
27 | if (!startStation || !endStation || !duration || !startTs) return
28 |
29 | process.stdout.write(`${startTs},${duration},${startStation},${endStation},${subscriber},${birthYear}\n`)
30 |
31 | if (!stations[startStation]) {
32 | stations[startStation] = [
33 | startStation,
34 | data['start station latitude'],
35 | data['start station longitude'],
36 | `"${data['start station name']}"`
37 | ].join(',')
38 | }
39 | if (!stations[endStation]) {
40 | stations[endStation] = [
41 | endStation,
42 | data['end station latitude'],
43 | data['end station longitude'],
44 | `"${data['end station name']}"`
45 | ].join(',')
46 | }
47 | }
48 |
49 | function onEnd () {
50 | const csvText = [
51 | stationsHeader,
52 | ...Object.keys(stations).map(id => stations[id])
53 | ].join('\n')
54 |
55 | fs.writeFileSync(STATIONS_OUT, csvText + '\n')
56 | }
57 |
58 | function int (val) {
59 | return parseInt(val, 10)
60 | }
61 |
--------------------------------------------------------------------------------
/scripts/download-data.sh:
--------------------------------------------------------------------------------
1 | # curl "https://s3.amazonaws.com/tripdata/201707-citibike-tripdata.csv.zip" > 201707-citibike-tripdata.csv.zip
2 | # curl "https://s3.amazonaws.com/tripdata/201706-citibike-tripdata.csv.zip" > 201706-citibike-tripdata.csv.zip
3 | # curl "https://s3.amazonaws.com/tripdata/201705-citibike-tripdata.csv.zip" > 201705-citibike-tripdata.csv.zip
4 | # curl "https://s3.amazonaws.com/tripdata/201704-citibike-tripdata.csv.zip" > 201704-citibike-tripdata.csv.zip
5 |
6 | # NOTE: columns for data before april 2017 may be named differently than scripts are expecting
7 |
8 | # curl "https://s3.amazonaws.com/tripdata/201703-citibike-tripdata.csv.zip" > 201703-citibike-tripdata.csv.zip
9 | # curl "https://s3.amazonaws.com/tripdata/201702-citibike-tripdata.csv.zip" > 201702-citibike-tripdata.csv.zip
10 | # curl "https://s3.amazonaws.com/tripdata/201701-citibike-tripdata.csv.zip" > 201701-citibike-tripdata.csv.zip
11 |
12 | # NOTE: pre-2017 data has dates represented a little differently and may not work seamlessly with this codebase
13 |
14 | # curl "https://s3.amazonaws.com/tripdata/201612-citibike-tripdata.zip" > 201612-citibike-tripdata.csv.zip
15 | # curl "https://s3.amazonaws.com/tripdata/201611-citibike-tripdata.zip" > 201611-citibike-tripdata.csv.zip
16 | # ... and so on
17 |
--------------------------------------------------------------------------------
/scripts/extract-coordinates-from-geojson.js:
--------------------------------------------------------------------------------
1 | // clean nyc-streets.geo.json
2 | const fs = require('fs')
3 | const path = require('path')
4 | const topojson = require('topojson')
5 | const argv = require('argv')
6 | const args = argv.option({
7 | name: 'help',
8 | short: 'h',
9 | type: 'boolean'
10 | }).run()
11 |
12 | const pathToGeoJSON = args.targets[0]
13 |
14 | if (args.options.help || !pathToGeoJSON) help()
15 |
16 | function help () {
17 | console.log(`usage: node ${path.basename(__filename)} PATH/TO/GEOJSON.json`)
18 | process.exit()
19 | }
20 |
21 | const json = fs.readFileSync(pathToGeoJSON, 'utf-8')
22 | const geoJSON = JSON.parse(json)
23 |
24 | if (geoJSON.type === 'Topology') {
25 | const result = topojson.mesh(geoJSON)
26 | console.log('TOPOLOGY! Check the code and figure out how to do this')
27 | console.log(result.coordinates.length)
28 | process.exit()
29 | }
30 |
31 | geoJSON.features.forEach(feat => {
32 | // filter to just BK and Manhattan streets
33 | // if (!['047', '061'].includes(feat.properties['COUNTYFP'])) return
34 | // if (feat.properties['COUNTYFP'] !== '061') return
35 | // if (feat.properties['MTFCC'] !== 'S1200' && feat.properties['MTFCC'] !== 'S1100') return
36 | if (feat.geometry.type === 'MultiLineString') feat.geometry.coordinates.forEach(processLine)
37 | if (feat.geometry.type === 'LineString') processLine(feat.geometry.coordinates)
38 | if (feat.geometry.type === 'Polygon') feat.geometry.coordinates.forEach(processLine)
39 | if (feat.geometry.type === 'MultiPolygon') feat.geometry.coordinates.forEach(c => c.forEach(processLine))
40 | })
41 |
42 | function processLine (line) {
43 | process.stdout.write(line.map(pt => pt.join(',')).join(';'))
44 | process.stdout.write('\n')
45 | }
46 |
--------------------------------------------------------------------------------
/scripts/process-trips.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # NOTE: this should be called from /data
4 |
5 | IN="201704-citibike-tripdata.csv"
6 | DATE="2017-04-18"
7 | OUT="trips-$DATE.csv"
8 | CLEANED_DIR="../cleaned"
9 |
10 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
11 |
12 | head -1 $IN > $OUT
13 | cat $IN | grep $DATE >> $OUT
14 | cat $OUT | node $DIR/clean-citibike-trips.js > $OUT.tmp
15 | head -1 $OUT.tmp > $CLEANED_DIR/$OUT
16 | cat $OUT.tmp | grep $DATE >> $CLEANED_DIR/$OUT
17 | rm $OUT.tmp $OUT
18 |
--------------------------------------------------------------------------------
/src/check-support.js:
--------------------------------------------------------------------------------
1 | module.exports = function checkSupport (regl) {
2 | const data = new Float32Array(4)
3 | data[0] = data[1] = data[2] = data[3] = 0.1234567
4 | const initialTexture = regl.texture({
5 | data: data,
6 | shape: [1, 1, 4],
7 | type: 'float'
8 | })
9 | try {
10 | regl.framebuffer({
11 | color: initialTexture,
12 | depth: false,
13 | stencil: false
14 | })
15 | } catch (err) {
16 | notSupported()
17 | throw new Error('visualization requires OES_texture_float webgl extension', err)
18 | }
19 | }
20 |
21 | function notSupported () {
22 | document.body.innerHTML = ''
23 | const warningDiv = document.body.appendChild(document.createElement('div'))
24 | warningDiv.classList.add('not-supported')
25 | warningDiv.innerHTML = 'This visualization requires a WebGL ' +
26 | 'extension that may not be available on mobile browsers. But you can take a look at ' +
27 | 'this little video I made of it!'
28 | }
29 |
--------------------------------------------------------------------------------
/src/create-buttons.js:
--------------------------------------------------------------------------------
1 | module.exports = function createButtons (container, settings) {
2 | const createBtnEl = () => document.createElement('span')
3 | const buttons = [
4 | { name: 'showPoints', label: 'Points', group: 'view-options', el: createBtnEl() },
5 | { name: 'showPaths', label: 'Paths', group: 'view-options', el: createBtnEl() },
6 | { name: 'curvedPaths', label: 'Arcs', group: 'view-options', el: createBtnEl() },
7 |
8 | { name: 'subscriber', label: 'Member', group: 'filter', el: createBtnEl() },
9 | { name: 'nonSubscriber', label: 'Day Pass', group: 'filter', el: createBtnEl() }
10 | ]
11 |
12 | const btnGroups = {}
13 | buttons.forEach(({ name, label, el, group }) => {
14 | btnGroups[group] = btnGroups[group] || createAndAppendButtonGroup(container, group)
15 | el.innerText = label
16 | btnGroups[group].appendChild(el)
17 | el.addEventListener('click', () => toggleFilter(name))
18 | })
19 |
20 | return function renderButtons (settings) {
21 | buttons.forEach(({ name, el }) => {
22 | if (settings[name]) {
23 | el.classList.add('highlight')
24 | } else {
25 | el.classList.remove('highlight')
26 | }
27 | })
28 | }
29 |
30 | function toggleFilter (name) {
31 | settings[name] = !settings[name]
32 | }
33 | }
34 |
35 | function createAndAppendButtonGroup (el, name) {
36 | const btnGroupEl = el.appendChild(document.createElement('div'))
37 | btnGroupEl.classList.add('button-group')
38 | btnGroupEl.classList.add(name)
39 | return btnGroupEl
40 | }
41 |
--------------------------------------------------------------------------------
/src/create-elapsed-time-view.js:
--------------------------------------------------------------------------------
1 | const { secondsToTime, getMeridian, convertHourFrom24H } = require('./helpers')
2 |
3 | module.exports = function createElapsedTimeView (el, trips) {
4 | // assuming trips are in order, we'll say the current time of the viz
5 | // is simply midnight of the day of the first trip in the list plus
6 | // elapsed time
7 | const firstTripStartDate = trips[0]['start_ts'].split(' ')[0]
8 | const vizStartTime = (new Date(`${firstTripStartDate}T00:00:00`)).getTime()
9 |
10 | const timeEl = el.querySelector('span.time')
11 | const meridanEl = el.querySelector('span.meridian')
12 | const dayEl = el.querySelector('.day')
13 | const dateEl = el.querySelector('.date')
14 |
15 | return function renderElapsedTime (elapsed) {
16 | const curDate = new Date(vizStartTime + elapsed * 1000)
17 | const curTime = secondsToTime(elapsed)
18 | dayEl.innerText = days[curDate.getDay()]
19 | dateEl.innerText = `${curDate.getDate()} ${months[curDate.getMonth()]} ${curDate.getFullYear()}`
20 | timeEl.innerText = convertHourFrom24H(curTime)
21 | meridanEl.innerText = getMeridian(curTime)
22 | }
23 | }
24 |
25 | const days = 'Sunday Monday Tuesday Wednesday Thursday Friday Saturday Sunday'.split(' ')
26 | const months = 'January February March April May June July August September October November December'.split(' ')
27 |
--------------------------------------------------------------------------------
/src/create-map-renderer.js:
--------------------------------------------------------------------------------
1 | const glslify = require('glslify')
2 |
3 | module.exports = function createMapRenderer (regl, lines) {
4 | const lineSegments = []
5 | for (let line of lines) {
6 | for (let j = 0; j < line.length - 1; j++) {
7 | lineSegments.push(line[j])
8 | lineSegments.push(line[j + 1])
9 | }
10 | }
11 |
12 | const renderMap = regl({
13 | vert: glslify.file('./map.vert'),
14 | frag: glslify.file('./simple.frag'),
15 | attributes: {
16 | position: lineSegments
17 | },
18 | count: lineSegments.length,
19 | uniforms: {
20 | color: [0.75, 0.75, 0.75]
21 | },
22 | primitive: 'lines'
23 | })
24 |
25 | const gridLines = []
26 | const separation = 0.05
27 | const limit = 2
28 | for (let j = -limit; j <= limit; j += separation) {
29 | gridLines.push([j, -limit, 0], [j, limit, 0], [-limit, j, 0], [limit, j, 0])
30 | }
31 |
32 | const renderGrid = regl({
33 | vert: glslify.file('./simple.vert'),
34 | frag: glslify.file('./simple.frag'),
35 | attributes: {
36 | position: gridLines
37 | },
38 | count: gridLines.length,
39 | uniforms: {
40 | color: [0.15, 0.15, 0.15, 0.25]
41 | },
42 | primitive: 'lines'
43 | })
44 |
45 | return () => {
46 | renderGrid()
47 | renderMap()
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/create-projection.js:
--------------------------------------------------------------------------------
1 | const mercator = require('projections/mercator')
2 |
3 | module.exports = function createProjection ({ bbox, zoom }) {
4 | const projectionOpts = {
5 | meridian: 180 + bbox[0],
6 | latLimit: bbox[3]
7 | }
8 | return function project ([lon, lat]) {
9 | const { x, y } = mercator({ lon, lat }, projectionOpts)
10 | return [x * zoom, y * zoom]
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/create-roaming-camera.js:
--------------------------------------------------------------------------------
1 | const { createSpring } = require('spring-animator')
2 | const createCamera = require('3d-view-controls')
3 | const { getIntersection } = require('./helpers')
4 |
5 | module.exports = function createRoamingCamera (canvas, focus, center, eye, getProjection) {
6 | let isRoaming = true
7 | let timeout
8 |
9 | canvas.addEventListener('mousedown', stopRoaming)
10 | canvas.addEventListener('dblclick', onDblClick)
11 |
12 | const camera = createCamera(canvas, {
13 | zoomSpeed: 4,
14 | distanceLimits: [0.05, 1.03]
15 | })
16 | const [fX, fY] = focus
17 | const cameraX = createSpring(0.005, 1.5, center[0])
18 | const cameraY = createSpring(0.005, 1.5, center[1])
19 | const cameraZ = createSpring(0.005, 1.5, center[2])
20 |
21 | const focusX = createSpring(0.05, 1.5, fX)
22 | const focusY = createSpring(0.05, 1.5, fY)
23 |
24 | camera.lookAt(
25 | center,
26 | eye,
27 | [0.52, -0.11, -99]
28 | )
29 |
30 | function onDblClick (e) {
31 | const [fX, fY] = getIntersection(
32 | [e.clientX, e.clientY],
33 | // prob not the best idea since elsewhere we are using `viewportWidth`
34 | // and `viewportHeight` passed by regl
35 | [0, 0, window.innerWidth, window.innerHeight],
36 | getProjection(),
37 | camera.matrix
38 | )
39 | setSpringsToCurrentCameraValues()
40 | focusX.updateValue(fX)
41 | focusY.updateValue(fY)
42 |
43 | // clear this text selection nonsense on screen after double click
44 | if (document.selection && document.selection.empty) {
45 | document.selection.empty()
46 | } else if (window.getSelection) {
47 | const sel = window.getSelection()
48 | sel.removeAllRanges()
49 | }
50 | }
51 |
52 | function setRandomCameraPosition () {
53 | // dont move focus too much because it has a much snappier spring
54 | const newFocusX = fX + (Math.random() - 0.5) * 0.01
55 | const newFocusY = fY + (Math.random() - 0.5) * 0.01
56 | focusX.updateValue(newFocusX)
57 | focusY.updateValue(newFocusY)
58 |
59 | cameraX.updateValue(newFocusX + Math.random() - 0.5)
60 | cameraY.updateValue(newFocusY + Math.random() - 0.5)
61 | cameraZ.updateValue(Math.random() * -0.5)
62 | }
63 |
64 | cameraRoamLoop()
65 | function cameraRoamLoop () {
66 | clearTimeout(timeout)
67 | timeout = setTimeout(cameraRoamLoop, 10000)
68 | setRandomCameraPosition()
69 | }
70 |
71 | function tick () {
72 | camera.tick()
73 | camera.up = [camera.up[0], camera.up[1], -999]
74 | camera.eye = [focusX.tick(), focusY.tick(), 0]
75 | if (isRoaming) {
76 | camera.center = [cameraX.tick(), cameraY.tick(), cameraZ.tick()]
77 | }
78 | }
79 | function getMatrix () {
80 | return camera.matrix
81 | }
82 | function getCenter () {
83 | return camera.center
84 | }
85 | function stopRoaming () {
86 | clearTimeout(timeout)
87 | timeout = null
88 | isRoaming = false
89 | }
90 | function startRoaming () {
91 | setSpringsToCurrentCameraValues()
92 | cameraRoamLoop()
93 | isRoaming = true
94 | }
95 |
96 | function setSpringsToCurrentCameraValues () {
97 | focusX.updateValue(camera.center[0], false)
98 | focusY.updateValue(camera.center[1], false)
99 |
100 | cameraX.updateValue(camera.eye[0], false)
101 | cameraY.updateValue(camera.eye[1], false)
102 | cameraZ.updateValue(camera.eye[2], false)
103 | }
104 |
105 | window.camera = camera
106 | return {
107 | tick,
108 | getMatrix,
109 | getCenter,
110 | startRoaming
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/src/create-state-transitioner.js:
--------------------------------------------------------------------------------
1 | const glslify = require('glslify')
2 | const newArray = require('new-array')
3 | const { getIntersection } = require('./helpers')
4 |
5 | module.exports = function createStateTransitioner (regl, trips, settings) {
6 | let mousePosition = [0, 0]
7 | regl._gl.canvas.addEventListener('mousemove', (e) => {
8 | mousePosition = [e.clientX, e.clientY]
9 | })
10 | let isShiftPressed = false
11 | document.addEventListener('keydown', (e) => {
12 | if (e.which === 16) isShiftPressed = true
13 | })
14 | document.addEventListener('keyup', (e) => {
15 | if (e.which === 16) isShiftPressed = false
16 | })
17 |
18 | const tripStateTextureSize = Math.ceil(Math.sqrt(trips.length))
19 | const tripStateTextureLength = tripStateTextureSize * tripStateTextureSize
20 | const initialTripState = new Float32Array(tripStateTextureLength * 4)
21 | for (let i = 0; i < tripStateTextureLength; ++i) {
22 | initialTripState[i * 4] = 0 // arcHeight
23 | initialTripState[i * 4 + 1] = 0 // pathAlpha
24 | initialTripState[i * 4 + 2] = 0 // pointSize
25 | }
26 |
27 | let prevTripStateTexture = createStateBuffer(initialTripState, tripStateTextureSize)
28 | let curTripStateTexture = createStateBuffer(initialTripState, tripStateTextureSize)
29 | let nextTripStateTexture = createStateBuffer(initialTripState, tripStateTextureSize)
30 |
31 | const stateIndexes = []
32 | const tripMetaDataState = new Float32Array(tripStateTextureLength * 4)
33 | for (let j = 0; j < trips.length; j++) {
34 | const tripStateIndexX = j % tripStateTextureSize
35 | const tripStateIndexY = j / tripStateTextureSize | 0
36 | stateIndexes.push([tripStateIndexX / tripStateTextureSize, tripStateIndexY / tripStateTextureSize])
37 | tripMetaDataState[j * 4] = trips[j].subscriber ? 1.0 : 0.0 // subscriber
38 | tripMetaDataState[j * 4 + 1] = trips[j].startPosition[0]
39 | tripMetaDataState[j * 4 + 2] = trips[j].startPosition[1]
40 | }
41 | const tripMetaDataTexture = createStateBuffer(tripMetaDataState, tripStateTextureSize)
42 |
43 | const dampening = 1.0
44 | const stiffness = 0.1
45 | const MAX_ARC_HEIGHT = 2.5
46 | const MAX_PT_SIZE = 3
47 |
48 | const updateState = regl({
49 | framebuffer: () => nextTripStateTexture,
50 |
51 | vert: glslify.file('./trip-state.vert'),
52 | frag: glslify.file('./trip-state.frag'),
53 |
54 | attributes: {
55 | position: [
56 | -1, -1,
57 | 1, -1,
58 | -1, 1,
59 | 1, 1
60 | ]
61 | },
62 |
63 | uniforms: {
64 | curTripStateTexture: () => curTripStateTexture,
65 | prevTripStateTexture: () => prevTripStateTexture,
66 | tripMetaDataTexture: tripMetaDataTexture,
67 | rayPicker: regl.prop('rayPicker'),
68 | rayPickerThreshold: regl.prop('rayPickerThreshold'),
69 | dampening: dampening,
70 | stiffness: stiffness,
71 | maxArcHeight: MAX_ARC_HEIGHT,
72 | maxPointSize: MAX_PT_SIZE,
73 | showSubscriber: regl.prop('showSubscriber'),
74 | showNonSubscriber: regl.prop('showNonSubscriber'),
75 | curvedPaths: regl.prop('curvedPaths'),
76 | showPaths: regl.prop('showPaths'),
77 | showPoints: regl.prop('showPoints')
78 | },
79 |
80 | count: 4,
81 | primitive: 'triangle strip'
82 | })
83 |
84 | const circleGranularity = 30
85 | const drawCircle = regl({
86 | vert: glslify.file('./pointer-circle.vert'),
87 | frag: glslify.file('./simple.frag'),
88 | attributes: {
89 | rads: newArray(circleGranularity + 1).map((_, i) => Math.PI * 2 * i / circleGranularity)
90 | },
91 | uniforms: {
92 | center: regl.prop('center'),
93 | size: regl.prop('size'),
94 | projection: regl.prop('projection'),
95 | view: regl.prop('view'),
96 | color: [0.8, 0.7, 1.0]
97 | },
98 | count: circleGranularity,
99 | primitive: 'line loop'
100 | })
101 |
102 | function getStateIndexes () {
103 | return stateIndexes
104 | }
105 |
106 | function tick (context) {
107 | const rayPickerThreshold = isShiftPressed ? context.rayPickerThreshold : 10
108 | const rayPicker = isShiftPressed ? getIntersection(
109 | mousePosition,
110 | context.viewport,
111 | context.projection,
112 | context.view
113 | ) || [0, 0, 0] : [0, 0, 0]
114 | cycleStates()
115 | updateState({
116 | showSubscriber: context.subscriber,
117 | showNonSubscriber: context.nonSubscriber,
118 | curvedPaths: context.curvedPaths,
119 | showPaths: context.showPaths,
120 | showPoints: context.showPoints,
121 | rayPickerThreshold: rayPickerThreshold,
122 | rayPicker: rayPicker
123 | })
124 | if (isShiftPressed) {
125 | drawCircle({
126 | center: rayPicker,
127 | size: rayPickerThreshold,
128 | projection: context.projection,
129 | view: context.view
130 | })
131 | }
132 | }
133 |
134 | function getStateTexture () {
135 | return curTripStateTexture
136 | }
137 |
138 | return {
139 | tick,
140 | getStateTexture,
141 | getStateIndexes
142 | }
143 |
144 | function createStateBuffer (initialState, textureSize) {
145 | const initialTexture = regl.texture({
146 | data: initialState,
147 | shape: [textureSize, textureSize, 4],
148 | type: 'float'
149 | })
150 | return regl.framebuffer({
151 | color: initialTexture,
152 | depth: false,
153 | stencil: false
154 | })
155 | }
156 |
157 | function cycleStates () {
158 | const tmp = prevTripStateTexture
159 | prevTripStateTexture = curTripStateTexture
160 | curTripStateTexture = nextTripStateTexture
161 | nextTripStateTexture = tmp
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/src/create-timeline.js:
--------------------------------------------------------------------------------
1 | const catRomSpline = require('cat-rom-spline')
2 | const newArray = require('new-array')
3 | const { createSpring } = require('spring-animator')
4 | const css = require('dom-css')
5 | const { getSeconds, secondsToTime, getMeridian, convertHourFrom24H } = require('./helpers')
6 |
7 | const verticalPadding = 20
8 |
9 | module.exports = function createTimeline (el, trips, vizDuration, setElapsed, settings) {
10 | const bucketGranularity = 60 * 30
11 | // get histogram buckets for both subscribers and non-subscribers
12 | const getBucket = getHistogramBucketsByFilter(trips, bucketGranularity, vizDuration)
13 | const svgEl = createSVG(el)
14 | const { width } = el.getBoundingClientRect()
15 |
16 | const updateHistogram = drawHistogram(svgEl, getBucket(settings))
17 | drawTimeline(svgEl, vizDuration)
18 | const updateScrubberPosition = createScrubber(svgEl, vizDuration)
19 |
20 | el.addEventListener('mousedown', mouseDown)
21 | el.addEventListener('mousemove', mouseMove)
22 | window.addEventListener('mouseup', mouseUp)
23 |
24 | let isMouseDown = false
25 | let curXPosition = 0
26 | function mouseDown (e) {
27 | isMouseDown = true
28 | curXPosition = Math.max(e.offsetX, 0)
29 | setElapsed(getTimeFromXPosition(curXPosition))
30 | }
31 |
32 | function mouseMove (e) {
33 | if (!isMouseDown) return
34 | curXPosition = Math.max(e.offsetX, 0)
35 | setElapsed(getTimeFromXPosition(curXPosition))
36 | }
37 |
38 | function mouseUp () {
39 | if (!isMouseDown) return
40 | isMouseDown = false
41 | }
42 |
43 | function getTimeFromXPosition (xPosition) {
44 | return xPosition / width * vizDuration
45 | }
46 |
47 | return function renderTimeline (elapsed, settings) {
48 | if (isMouseDown) {
49 | elapsed = getTimeFromXPosition(curXPosition)
50 | setElapsed(elapsed)
51 | }
52 | updateScrubberPosition(elapsed)
53 | updateHistogram(getBucket(settings))
54 | }
55 | }
56 |
57 | function getHistogramBucketsByFilter (trips, granularity, vizDuration) {
58 | const bucketsByFilter = {
59 | subscriber: [],
60 | nonSubscriber: []
61 | }
62 | const maxBuckets = vizDuration / granularity
63 | for (let trip of trips) {
64 | // +1 to every bucket that this trip touches
65 | const startBucket = getSeconds(trip.start_ts) / granularity | 0
66 | const endBucket = Math.min(maxBuckets, Math.ceil(trip.duration / granularity) + startBucket)
67 | const filter = trip.subscriber ? 'subscriber' : 'nonSubscriber'
68 | const buckets = bucketsByFilter[filter]
69 | for (let i = startBucket; i <= endBucket; i++) {
70 | buckets[i] = buckets[i] || 0
71 | buckets[i] += 1
72 | }
73 | }
74 |
75 | return (settings) => {
76 | const buckets = newArray(maxBuckets).map(() => 0)
77 | for (let filter in bucketsByFilter) {
78 | if (!settings[filter]) continue
79 | for (let i = 0; i < buckets.length; i++) {
80 | buckets[i] += bucketsByFilter[filter][i]
81 | }
82 | }
83 | return buckets
84 | }
85 | }
86 |
87 | function createSVG (el) {
88 | const { width, height } = el.getBoundingClientRect()
89 | const svgEl = el.appendChild(document.createElementNS('http://www.w3.org/2000/svg', 'svg'))
90 | svgEl.setAttribute('width', width)
91 | svgEl.setAttribute('height', height)
92 | css(svgEl, { position: 'absolute', top: 0, left: 0 })
93 | return svgEl
94 | }
95 |
96 | function createScrubber (svgEl, vizDuration) {
97 | const { width, height } = svgEl.getBoundingClientRect()
98 | const lineEl = document.createElementNS('http://www.w3.org/2000/svg', 'line')
99 | lineEl.setAttribute('stroke', '#dbb685')
100 | lineEl.setAttribute('stroke-width', 2)
101 | lineEl.setAttribute('x1', 0)
102 | lineEl.setAttribute('y1', verticalPadding)
103 | lineEl.setAttribute('x2', 0)
104 | lineEl.setAttribute('y2', height - verticalPadding)
105 | svgEl.appendChild(lineEl)
106 | return function updatePosition (elapsed) {
107 | const x = elapsed / vizDuration * width
108 | lineEl.setAttribute('x1', x)
109 | lineEl.setAttribute('x2', x)
110 | }
111 | }
112 |
113 | function drawTimeline (svgEl, vizDuration) {
114 | const { width, height } = svgEl.getBoundingClientRect()
115 | const tickSize = 3 * 60 * 60
116 | const tickCount = vizDuration / tickSize // not including last tick
117 | const startY = height - verticalPadding + 10 // 10 is the max tick height
118 | for (let i = 0; i <= tickCount; i++) {
119 | const x = i / tickCount * width
120 | const lineLength = i === tickCount / 2 ? 10 : i === tickCount / 4 ? 7 : 5
121 | const lineEl = document.createElementNS('http://www.w3.org/2000/svg', 'line')
122 | lineEl.setAttribute('stroke', '#ddd')
123 | lineEl.setAttribute('x1', x)
124 | lineEl.setAttribute('y1', startY)
125 | lineEl.setAttribute('x2', x)
126 | lineEl.setAttribute('y2', startY + lineLength)
127 | svgEl.appendChild(lineEl)
128 |
129 | const timeEl = document.createElement('span')
130 | const time = secondsToTime(i / tickCount * vizDuration)
131 | const hour = convertHourFrom24H(time).split(':')[0]
132 | const meridian = getMeridian(time)
133 | timeEl.innerText = `${hour}${meridian}`
134 | const elWidth = 50
135 | css(timeEl, {
136 | position: 'absolute',
137 | top: height,
138 | left: x - elWidth / 2,
139 | width: elWidth,
140 | fontSize: 12,
141 | textAlign: 'center'
142 | })
143 | svgEl.parentElement.appendChild(timeEl)
144 | }
145 | const lineEl = document.createElementNS('http://www.w3.org/2000/svg', 'line')
146 | lineEl.setAttribute('stroke', '#ddd')
147 | lineEl.setAttribute('x1', 0)
148 | lineEl.setAttribute('y1', startY - 1)
149 | lineEl.setAttribute('x2', width)
150 | lineEl.setAttribute('y2', startY - 1)
151 | svgEl.appendChild(lineEl)
152 | }
153 |
154 | function drawHistogram (svgEl, buckets) {
155 | const { width, height } = svgEl.getBoundingClientRect()
156 | const max = buckets.reduce((curMax, val) => Math.max(curMax, val), -Infinity)
157 | const histogramHeight = height - verticalPadding * 2
158 |
159 | const pathEl = document.createElementNS('http://www.w3.org/2000/svg', 'path')
160 | pathEl.setAttribute('fill', 'rgba(255, 255, 255, 0.6)')
161 | pathEl.setAttribute('stroke', '#ddd')
162 | svgEl.appendChild(pathEl)
163 |
164 | const controlsSprings = buckets.map(() => createSpring(0.1, 1.1, 0))
165 |
166 | updateHistogram(buckets)
167 |
168 | function getDAttribute (points) {
169 | let d = ''
170 | d += `M0,${histogramHeight + verticalPadding} `
171 | points.forEach(p => {
172 | d += `L${p[0]},${p[1]} `
173 | })
174 | d += `L${points[points.length - 1][0]},${histogramHeight + verticalPadding}`
175 | return d
176 | }
177 |
178 | function updateHistogram (buckets) {
179 | controlsSprings.forEach((spring, i) => spring.updateValue(buckets[i]))
180 | const controls = controlsSprings.map(spring => spring.tick()).map((v, i) => [
181 | i / (buckets.length - 1) * width,
182 | (histogramHeight + verticalPadding) - (v / max * histogramHeight)
183 | ])
184 | controls.unshift(controls[0].map(v => v + 1))
185 | controls.push(controls[controls.length - 1].map(v => v + 1))
186 | const points = catRomSpline(controls, { samples: 2 })
187 | pathEl.setAttribute('d', getDAttribute(points))
188 | }
189 |
190 | return updateHistogram
191 | }
192 |
--------------------------------------------------------------------------------
/src/create-trip-paths-renderer.js:
--------------------------------------------------------------------------------
1 | const glslify = require('glslify')
2 | const vec2 = require('gl-vec2')
3 | const lerp = require('lerp')
4 | const { getSeconds } = require('./helpers')
5 |
6 | module.exports = function createTripPathsRenderer (regl, points) {
7 | const startColor = [235, 127, 0]
8 | const endColor = [172, 240, 242]
9 | function getColor (t) {
10 | return [
11 | lerp(startColor[0], endColor[0], t) / 255,
12 | lerp(startColor[1], endColor[1], t) / 255,
13 | lerp(startColor[2], endColor[2], t) / 255
14 | ]
15 | }
16 |
17 | function getPosition (start, end, t) {
18 | const position = vec2.lerp([], start, end, t)
19 | const z = Math.sin(t * Math.PI) * -0.1 * Math.max(0.04, vec2.distance(start, end))
20 | position.push(z)
21 | return position
22 | }
23 |
24 | // because drawing in lines mode takes a start point, then an end point,
25 | // then the next line's start, and then end - we have to reorg these points
26 | // to work with the shader
27 | const linesPoints = []
28 | const linesColors = []
29 | const linesStartTimes = []
30 | const linesDurations = []
31 | const linesTripStateIndex = []
32 | points.forEach(p => {
33 | const arcPoints = 25
34 | const startTime = getSeconds(p.start_ts)
35 | for (let j = 0; j < arcPoints; j++) {
36 | linesPoints.push(getPosition(p.startPosition, p.endPosition, j / arcPoints))
37 | linesColors.push(getColor(j / arcPoints))
38 | linesStartTimes.push(startTime)
39 | linesDurations.push(p.duration)
40 | linesTripStateIndex.push(p.tripStateIndex)
41 |
42 | linesPoints.push(getPosition(p.startPosition, p.endPosition, (j + 1) / arcPoints))
43 | linesColors.push(getColor((j + 1) / arcPoints))
44 | linesStartTimes.push(startTime)
45 | linesDurations.push(p.duration)
46 | linesTripStateIndex.push(p.tripStateIndex)
47 | }
48 | })
49 |
50 | console.log('line segment points', linesPoints.length)
51 |
52 | return regl({
53 | vert: glslify.file('./trip-path.vert'),
54 | frag: glslify.file('./trip-path.frag'),
55 |
56 | attributes: {
57 | position: linesPoints,
58 | color: linesColors,
59 | startTime: linesStartTimes,
60 | duration: linesDurations,
61 | tripStateIndex: linesTripStateIndex
62 | },
63 |
64 | count: linesPoints.length,
65 |
66 | primitive: 'lines'
67 | })
68 | }
69 |
--------------------------------------------------------------------------------
/src/create-trip-points-renderer.js:
--------------------------------------------------------------------------------
1 | const glslify = require('glslify')
2 | const { getSeconds } = require('./helpers')
3 |
4 | module.exports = function createTripPointsRenderer (regl, points) {
5 | return regl({
6 | vert: glslify.file('./trip-points.vert'),
7 | frag: glslify.file('./trip-points.frag'),
8 |
9 | attributes: {
10 | startPosition: points.map(p => p.startPosition),
11 | endPosition: points.map(p => p.endPosition),
12 | color: points.map(p => p.subscriber ? [0.7, 0.7, 1, 1] : [1, 0.7, 0.7, 1]),
13 | startTime: points.map(p => getSeconds(p.start_ts)),
14 | duration: points.map(p => p.duration),
15 | tripStateIndex: points.map(p => p.tripStateIndex)
16 | },
17 |
18 | count: points.length,
19 |
20 | primitive: 'point'
21 | })
22 | }
23 |
--------------------------------------------------------------------------------
/src/helpers.js:
--------------------------------------------------------------------------------
1 | const mat4 = require('gl-mat4')
2 | const intersect = require('ray-plane-intersection')
3 | const pickRay = require('camera-picking-ray')
4 |
5 | module.exports = {
6 | extent,
7 | parseLines,
8 | parseTripsCSV,
9 | parseStationsCSV,
10 | mungeStationsIntoTrip,
11 | getSeconds,
12 | getMeridian,
13 | convertHourFrom24H,
14 | secondsToTime,
15 | getIntersection
16 | }
17 |
18 | function extent (lines) {
19 | let minLon, minLat, maxLon, maxLat
20 | for (let points of lines) {
21 | for (let pt of points) {
22 | if (!minLon || pt[0] < minLon) minLon = pt[0]
23 | if (!maxLon || pt[0] > maxLon) maxLon = pt[0]
24 | if (!minLat || pt[1] < minLat) minLat = pt[1]
25 | if (!maxLat || pt[1] > maxLat) maxLat = pt[1]
26 | }
27 | }
28 | return [minLon, minLat, maxLon, maxLat]
29 | }
30 |
31 | function parseLines (data) {
32 | return data
33 | .split('\n')
34 | .map(l => l.split(';')
35 | .map(pts => pts.split(',')
36 | .map(v => parseFloat(v)).filter(v => v)
37 | ).filter(l => l.length)
38 | ).filter(l => l.length)
39 | }
40 |
41 | function parseCSV (data, formatValue) {
42 | const lines = data.split('\n')
43 | const header = lines[0]
44 | const columns = header.split(',')
45 | return lines.slice(1).filter(l => l).map(line => {
46 | const obj = {}
47 | line.split(',').forEach((value, i) => {
48 | const name = columns[i]
49 | obj[name] = formatValue(value, name)
50 | })
51 | return obj
52 | })
53 | }
54 |
55 | function parseTripsCSV (data) {
56 | return parseCSV(data, (value, name) => {
57 | if (['birth_year', 'duration', 'end_station', 'start_station'].includes(name)) {
58 | return parseInt(value, 10)
59 | }
60 | if (name === 'subscriber') return (value === '1')
61 | return value
62 | })
63 | }
64 |
65 | function parseStationsCSV (data) {
66 | const stations = {}
67 | parseCSV(data, (value, name) => {
68 | if (['latitude', 'longitude'].includes(name)) {
69 | return parseFloat(value)
70 | }
71 | if (name === 'station_id') return parseInt(value)
72 | // strip out the opening and closing quotes
73 | if (name === 'name') return value.slice(1, value.length - 1)
74 | return value
75 | }).forEach(station => {
76 | stations[station.station_id] = station
77 | })
78 | return stations
79 | }
80 |
81 | function mungeStationsIntoTrip (trips, stations) {
82 | const filteredTrips = []
83 | const knownMissingStations = {}
84 | for (let trip of trips) {
85 | const startStation = stations[trip.start_station]
86 | const endStation = stations[trip.end_station]
87 | if (!startStation) {
88 | if (!knownMissingStations[trip.start_station]) {
89 | console.warn(`station ${trip.start_station} not found in stations list`)
90 | }
91 | knownMissingStations[trip.start_station] = true
92 | continue
93 | }
94 | if (!endStation) {
95 | if (!knownMissingStations[trip.end_station]) {
96 | console.warn(`station ${trip.end_station} not found in stations list`)
97 | }
98 | knownMissingStations[trip.end_station] = true
99 | continue
100 | }
101 | trip.start_station = getLatLonFromStation(startStation)
102 | trip.end_station = getLatLonFromStation(endStation)
103 | filteredTrips.push(trip)
104 | }
105 | if (trips.length - filteredTrips.length > 0) {
106 | console.warn(`filtered out ${trips.length - filteredTrips.length} trips`)
107 | }
108 | return filteredTrips
109 | }
110 |
111 | function getLatLonFromStation (station) {
112 | const { latitude, longitude } = station
113 | return [longitude, latitude]
114 | }
115 |
116 | // gets seconds from day start
117 | function getSeconds (datetimeString) {
118 | const [, time] = datetimeString.split(' ')
119 | const [hours, minutes, seconds] = time.split(':').map(n => parseInt(n, 10))
120 | return seconds + minutes * 60 + hours * 3600
121 | }
122 |
123 | function secondsToTime (seconds) {
124 | seconds = seconds | 0
125 | const hours = seconds / 3600 | 0
126 | seconds = seconds % 3600
127 | const minutes = seconds / 60 | 0
128 | const time = [hours, leftPad(minutes, 2, 0)].join(':')
129 | return `${time}`
130 | }
131 |
132 | function getMeridian (time) {
133 | const hours = parseInt(time.split(':')[0], 10) % 24
134 | return hours > 11 ? 'PM' : 'AM'
135 | }
136 |
137 | function convertHourFrom24H (time) {
138 | let [hours, minutes] = time.split(':')
139 | hours = parseInt(hours, 10)
140 | if (hours === 0) return `12:${minutes}`
141 | if (hours > 12) return `${hours - 12}:${minutes}`
142 | return time
143 | }
144 |
145 | function leftPad (val, len, char) {
146 | val = `${val}`
147 | while (val.length < len) {
148 | val = `${char}${val}`
149 | }
150 | return val
151 | }
152 |
153 | function getIntersection (mouse, viewport, projection, view) {
154 | const projView = mat4.multiply([], projection, view)
155 | const invProjView = mat4.invert([], projView)
156 | const rayOrigin = []
157 | const rayDir = []
158 | pickRay(rayOrigin, rayDir, mouse, viewport, invProjView)
159 | const normal = [0, 0, -1]
160 | const distance = 0
161 | return intersect([], rayOrigin, rayDir, normal, distance)
162 | }
163 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | /* global fetch */
2 |
3 | const createRegl = require('regl')
4 | const fit = require('canvas-fit')
5 | const css = require('dom-css')
6 | const mat4 = require('gl-mat4')
7 | const createProjection = require('./create-projection')
8 | const createTripPointsRenderer = require('./create-trip-points-renderer')
9 | const createTripPathsRenderer = require('./create-trip-paths-renderer')
10 | const createStateTransitioner = require('./create-state-transitioner')
11 | const createMapRenderer = require('./create-map-renderer')
12 | const createElapsedTimeView = require('./create-elapsed-time-view')
13 | const createTimeline = require('./create-timeline')
14 | const createButtons = require('./create-buttons')
15 | const createRoamingCamera = require('./create-roaming-camera')
16 | const checkSupport = require('./check-support')
17 | const setupDatGUI = require('./setup-dat-gui')
18 | const {
19 | extent,
20 | parseLines,
21 | parseTripsCSV,
22 | parseStationsCSV,
23 | mungeStationsIntoTrip
24 | } = require('./helpers')
25 |
26 | const vizDuration = 24 * 60 * 60
27 | const BG_COLOR = [0.22, 0.22, 0.22, 1]
28 | const rgba = `rgba(${BG_COLOR.slice(0, 3).map(v => v * 256 | 0).join(',')}, ${BG_COLOR[3]})`
29 | css(document.body, 'background-color', rgba)
30 |
31 | const appContainer = document.querySelector('.app-container')
32 | const canvas = appContainer.querySelector('.visualization').appendChild(document.createElement('canvas'))
33 | const regl = createRegl({
34 | extensions: 'OES_texture_float',
35 | canvas: canvas
36 | })
37 |
38 | checkSupport(regl)
39 |
40 | window.addEventListener('resize', fit(canvas), false)
41 |
42 | const nycStreetsFile = './cleaned/nyc-streets.txt'
43 | const tripsFile = './cleaned/trips-2017-06-09.csv'
44 | const stationsFile = './cleaned/stations.csv'
45 |
46 | Promise.all([
47 | fetch(nycStreetsFile).then(d => d.text()).then(parseLines),
48 | fetch(tripsFile).then(d => d.text()).then(parseTripsCSV),
49 | fetch(stationsFile).then(d => d.text()).then(parseStationsCSV)
50 | ]).then(function onLoad ([coordinates, trips, stations]) {
51 | trips = mungeStationsIntoTrip(trips, stations)
52 |
53 | const projectCoords = createProjection({ bbox: extent(coordinates), zoom: 1300 })
54 | const lines = coordinates.map(points => points.map(projectCoords))
55 |
56 | const getProjection = () => mat4.perspective(
57 | [],
58 | Math.PI / 4,
59 | window.innerWidth / window.innerHeight,
60 | 0.01,
61 | 10
62 | )
63 | const focus = projectCoords([-74.006861, 40.724130]) // holland tunnel
64 | const [fX, fY] = focus
65 | const center = [fX - 0.15, fY + 0.15, -0.2] // i feel like these are misnamed in the 3d controls lib
66 | const eye = [fX, fY, 0] // i feel like these are misnamed in the 3d controls lib
67 | const camera = createRoamingCamera(canvas, focus, center, eye, getProjection)
68 |
69 | let settings = setupDatGUI({
70 | speed: [60 * 15, 1, 7000, 1],
71 | rayPickerThreshold: [0.05, 0.01, 0.1, 0.01],
72 | startRoaming: camera.startRoaming,
73 | 'start/stop': () => { paused = !paused }
74 | })
75 |
76 | settings = Object.assign(settings, {
77 | showPoints: true,
78 | showPaths: true,
79 | curvedPaths: true,
80 | subscriber: true,
81 | nonSubscriber: true
82 | })
83 |
84 | let elapsed = 4 * 60 * 60 // start at 4:00 so we get into the action a lil' faster
85 | const setElapsed = (newElapsed) => { elapsed = newElapsed }
86 | const timelineEl = document.querySelector('.timeline')
87 | const renderTimeline = createTimeline(timelineEl, trips, vizDuration, setElapsed, settings)
88 | const renderElapsedTime = createElapsedTimeView(document.querySelector('.clock'), trips)
89 | const renderButtons = createButtons(document.querySelector('.buttons'), settings)
90 |
91 | for (let j = 0; j < trips.length; j++) {
92 | const trip = trips[j]
93 | trip.startPosition = projectCoords(trip.start_station)
94 | trip.endPosition = projectCoords(trip.end_station)
95 | }
96 |
97 | const stateTransitioner = createStateTransitioner(regl, trips, settings)
98 | const stateIndexes = stateTransitioner.getStateIndexes()
99 |
100 | for (let j = 0; j < trips.length; j++) {
101 | const trip = trips[j]
102 | trip.tripStateIndex = stateIndexes[j]
103 | }
104 |
105 | const drawTripPoints = createTripPointsRenderer(regl, trips)
106 | const drawTripPaths = createTripPathsRenderer(regl, trips)
107 | const renderMap = createMapRenderer(regl, lines)
108 |
109 | const globalRender = regl({
110 | uniforms: {
111 | projection: regl.prop('projection'),
112 | view: regl.prop('view'),
113 | elapsed: regl.prop('elapsed'),
114 | center: regl.prop('center'),
115 | tripStateTexture: () => stateTransitioner.getStateTexture()
116 | },
117 | blend: {
118 | enable: true,
119 | func: {
120 | srcRGB: 'src alpha',
121 | dstRGB: 1,
122 | srcAlpha: 1,
123 | dstAlpha: 1
124 | },
125 | equation: {
126 | rgb: 'add',
127 | alpha: 'add'
128 | }
129 | }
130 | })
131 |
132 | removeLoader()
133 | appContainer.classList.remove('hidden')
134 | let lastTime = 0
135 | let paused = false
136 | regl.frame(({ time }) => {
137 | if (!paused) {
138 | const timeDiff = (time - lastTime) * settings.speed
139 | elapsed = (elapsed + timeDiff) % vizDuration
140 | }
141 | lastTime = time
142 |
143 | regl.clear({
144 | color: BG_COLOR,
145 | depth: 1
146 | })
147 |
148 | const view = camera.getMatrix()
149 |
150 | camera.tick(settings)
151 |
152 | stateTransitioner.tick(Object.assign({
153 | projection: getProjection(),
154 | view: view,
155 | viewport: [0, 0, window.innerWidth, window.innerHeight]
156 | }, settings))
157 |
158 | renderButtons(settings)
159 | renderTimeline(elapsed, settings)
160 | renderElapsedTime(elapsed)
161 | globalRender({
162 | elapsed: elapsed,
163 | center: camera.getCenter(),
164 | view: view,
165 | projection: getProjection()
166 | }, () => {
167 | renderMap()
168 | drawTripPoints()
169 | drawTripPaths()
170 | })
171 | })
172 | })
173 |
174 | function removeLoader () {
175 | const loader = document.querySelector('.loader')
176 | loader.parentElement.removeChild(loader)
177 | }
178 |
--------------------------------------------------------------------------------
/src/map.vert:
--------------------------------------------------------------------------------
1 | attribute vec2 position;
2 |
3 | varying vec4 fragColor;
4 |
5 | uniform mat4 projection;
6 | uniform mat4 view;
7 | uniform vec3 color;
8 | uniform vec3 center;
9 |
10 | void main() {
11 | float d = distance(position, center.xy);
12 | float alpha = (1.0 - smoothstep(0.1, 0.8, d)) / 1.5;
13 | fragColor = vec4(color, alpha);
14 | gl_Position = projection * view * vec4(position, 0.0, 1.0);
15 | }
16 |
--------------------------------------------------------------------------------
/src/pointer-circle.vert:
--------------------------------------------------------------------------------
1 | attribute float rads;
2 |
3 | varying vec4 fragColor;
4 |
5 | uniform vec3 center;
6 | uniform float size;
7 | uniform mat4 projection;
8 | uniform mat4 view;
9 | uniform vec3 color;
10 |
11 | void main() {
12 | vec3 position = vec3(cos(rads), sin(rads), 0.0) * size + center;
13 | fragColor = vec4(color, 0.8);
14 | gl_Position = projection * view * vec4(position.xy, 0.001, 1.0);
15 | }
16 |
--------------------------------------------------------------------------------
/src/setup-dat-gui.js:
--------------------------------------------------------------------------------
1 | const { GUI } = require('dat-gui')
2 |
3 | module.exports = function guiSettings (settings) {
4 | const settingsObj = {}
5 | const gui = new GUI()
6 | gui.closed = true
7 | for (let key in settings) {
8 | if (typeof settings[key] === 'function') {
9 | gui.add({ [key]: settings[key] }, key)
10 | continue
11 | }
12 | settingsObj[key] = settings[key][0]
13 | const setting = gui
14 | .add(settingsObj, key, settings[key][1], settings[key][2])
15 | if (settings[key][3]) {
16 | setting.step(settings[key][3])
17 | }
18 | }
19 | return settingsObj
20 | }
21 |
--------------------------------------------------------------------------------
/src/simple.frag:
--------------------------------------------------------------------------------
1 | precision highp float;
2 |
3 | varying vec4 fragColor;
4 |
5 | void main() {
6 | gl_FragColor = fragColor;
7 | }
8 |
--------------------------------------------------------------------------------
/src/simple.vert:
--------------------------------------------------------------------------------
1 | attribute vec3 position;
2 |
3 | varying vec4 fragColor;
4 |
5 | uniform mat4 projection;
6 | uniform mat4 view;
7 | uniform vec4 color;
8 |
9 | void main() {
10 | fragColor = color;
11 | gl_Position = projection * view * vec4(position, 1.0);
12 | }
13 |
--------------------------------------------------------------------------------
/src/trip-path.frag:
--------------------------------------------------------------------------------
1 | precision highp float;
2 |
3 | varying vec4 fragColor;
4 | varying float discardMe;
5 |
6 | void main() {
7 | if (fragColor[3] == 0.0 || discardMe == 1.0) {
8 | discard;
9 | }
10 | gl_FragColor = fragColor;
11 | }
12 |
--------------------------------------------------------------------------------
/src/trip-path.vert:
--------------------------------------------------------------------------------
1 | attribute vec3 position;
2 | attribute vec3 color;
3 | attribute float startTime;
4 | attribute float duration;
5 | attribute vec2 tripStateIndex;
6 |
7 | varying vec4 fragColor;
8 | varying float discardMe;
9 |
10 | uniform mat4 projection;
11 | uniform mat4 view;
12 | uniform float elapsed;
13 | uniform sampler2D tripStateTexture;
14 |
15 | void main() {
16 | vec4 thisTripState = texture2D(tripStateTexture, tripStateIndex);
17 | float arcHeight = thisTripState.x;
18 | float pathAlpha = thisTripState.y;
19 | float middle = startTime + duration / 2.0;
20 | float endTime = startTime + duration;
21 | float buf = 200.0;
22 | float t;
23 | if (elapsed < middle) {
24 | t = smoothstep(startTime - buf / 2.0, middle, elapsed);
25 | } else {
26 | t = 1.0 - smoothstep(middle, endTime + buf, elapsed);
27 | }
28 | float w = 1.0;
29 | discardMe = 0.0;
30 | float alpha = t / 1.3 * pathAlpha;
31 | if (t == 0.0) {
32 | w = 0.0;
33 | discardMe = 1.0;
34 | alpha = 0.0;
35 | }
36 | float z = position.z * arcHeight;
37 | fragColor = vec4(color.rgb, alpha);
38 | gl_Position = projection * view * vec4(position.xy, z, w);
39 | }
40 |
--------------------------------------------------------------------------------
/src/trip-points.frag:
--------------------------------------------------------------------------------
1 | precision highp float;
2 |
3 | varying vec4 fragColor;
4 |
5 | void main() {
6 | gl_FragColor = fragColor;
7 | }
8 |
--------------------------------------------------------------------------------
/src/trip-points.vert:
--------------------------------------------------------------------------------
1 | attribute vec2 startPosition;
2 | attribute vec2 endPosition;
3 | attribute vec4 color;
4 | attribute float startTime;
5 | attribute float duration;
6 | attribute vec2 tripStateIndex;
7 |
8 | varying vec4 fragColor;
9 |
10 | uniform mat4 projection;
11 | uniform mat4 view;
12 | uniform float elapsed;
13 |
14 | uniform sampler2D tripStateTexture;
15 |
16 | void main() {
17 | vec4 thisTripState = texture2D(tripStateTexture, tripStateIndex);
18 | float arcHeight = thisTripState.x;
19 | float pointSize = thisTripState.z;
20 | fragColor = color;
21 | float endTime = startTime + duration;
22 | float t = smoothstep(startTime, endTime, elapsed);
23 | float z = arcHeight * sin(3.1415 * t) * max(distance(startPosition, endPosition), 0.04) * -0.1;
24 | vec3 newPosition = vec3(mix(startPosition, endPosition, t), z);
25 | float w = 1.0;
26 | if (t == 0.0 || t == 1.0) {
27 | w = 0.0;
28 | }
29 | gl_PointSize = pointSize;
30 | gl_Position = projection * view * vec4(newPosition, w);
31 | }
32 |
--------------------------------------------------------------------------------
/src/trip-state.frag:
--------------------------------------------------------------------------------
1 | precision mediump float;
2 |
3 | uniform sampler2D curTripStateTexture;
4 | uniform sampler2D prevTripStateTexture;
5 | uniform sampler2D tripMetaDataTexture;
6 | uniform vec3 rayPicker;
7 | uniform float rayPickerThreshold;
8 | uniform float tick;
9 | uniform float dampening;
10 | uniform float stiffness;
11 | uniform float maxArcHeight;
12 | uniform float maxPointSize;
13 | uniform bool showSubscriber;
14 | uniform bool showNonSubscriber;
15 | uniform bool curvedPaths;
16 | uniform bool showPaths;
17 | uniform bool showPoints;
18 |
19 | varying vec2 tripStateIndex;
20 |
21 | float getNextValue(float cur, float prev, float dest) {
22 | float velocity = cur - prev;
23 | float delta = dest - cur;
24 | float spring = delta * stiffness;
25 | float damper = velocity * -1.0 * dampening;
26 | return spring + damper + velocity + cur;
27 | }
28 |
29 | void main() {
30 | vec4 curState = texture2D(curTripStateTexture, tripStateIndex);
31 | vec4 prevState = texture2D(prevTripStateTexture, tripStateIndex);
32 | vec4 tripMetaData = texture2D(tripMetaDataTexture, tripStateIndex);
33 |
34 | bool isSubscriber = tripMetaData.x == 1.0;
35 | vec2 tripStart = tripMetaData.yz;
36 |
37 | bool isNearRayPicker = distance(tripStart, rayPicker.xy) < rayPickerThreshold;
38 |
39 | float destArcHeight = 0.0;
40 | float destPathAlpha = 0.0;
41 | float destPointSize = 0.0;
42 |
43 | if (isSubscriber) {
44 | if (isNearRayPicker && showSubscriber && curvedPaths) destArcHeight = maxArcHeight;
45 | if (isNearRayPicker && showSubscriber && showPaths) destPathAlpha = 1.0;
46 | if (isNearRayPicker && showSubscriber && showPoints) destPointSize = maxPointSize;
47 | } else {
48 | if (isNearRayPicker && showNonSubscriber && curvedPaths) destArcHeight = maxArcHeight;
49 | if (isNearRayPicker && showNonSubscriber && showPaths) destPathAlpha = 1.0;
50 | if (isNearRayPicker && showNonSubscriber && showPoints) destPointSize = maxPointSize;
51 | }
52 |
53 | float arcHeight = getNextValue(curState.x, prevState.x, destArcHeight);
54 | float pathAlpha = getNextValue(curState.y, prevState.y, destPathAlpha);
55 | float pointSize = getNextValue(curState.z, prevState.z, destPointSize);
56 |
57 | gl_FragColor = vec4(arcHeight, pathAlpha, pointSize, 0.0);
58 | }
59 |
--------------------------------------------------------------------------------
/src/trip-state.vert:
--------------------------------------------------------------------------------
1 | precision mediump float;
2 |
3 | attribute vec2 position;
4 |
5 | varying vec2 tripStateIndex;
6 |
7 | void main() {
8 | // map bottom left -1,-1 (normalized device coords) to 0,0 (particle texture index)
9 | // and 1,1 (ndc) to 1,1 (texture)
10 | tripStateIndex = 0.5 * (1.0 + position);
11 | gl_Position = vec4(position, 0, 1);
12 | }
13 |
--------------------------------------------------------------------------------