266 def is_weekend_night(time): |
266 def is_weekend_night(time): |
267 from datetime import timedelta |
267 from datetime import timedelta |
268 adjusted_time = time - timedelta(hours = 4, minutes = 30) |
268 adjusted_time = time - timedelta(hours = 4, minutes = 30) |
269 return adjusted_time.weekday() in [4, 5] and is_night_time(time) |
269 return adjusted_time.weekday() in [4, 5] and is_night_time(time) |
270 |
270 |
271 def describe(bus_stop): |
271 encircled = '\u24b6\u24b7\u24b8\u24b9\u24ba\u24bb\u24bc\u24bd\u24be\u24bf' \ |
|
272 '\u24c0\u24c1\u24c2\u24c3\u24c4\u24c5\u24c6\u24c7\u24c8\u24c9\u24ca\u24cb' \ |
|
273 '\u24cc\u24cd\u24ce\u24cf' |
|
274 |
|
275 def encircle(char): |
|
276 from string import ascii_uppercase |
|
277 try: |
|
278 return encircled[ascii_uppercase.index(char.upper())] |
|
279 except IndexError: |
|
280 return char |
|
281 |
|
282 def variant_abbreviations(variants): |
|
283 '''Makes a mapping of route destination variants to letter abbreviations''' |
|
284 suggestion = [variant[-1][0].upper() for variant in variants] |
|
285 if len(set(suggestion)) != len(suggestion): |
|
286 from string import ascii_uppercase |
|
287 suggestion = ascii_uppercase |
|
288 return dict(zip(variants, suggestion)) |
|
289 |
|
290 def schedule_entry_hash(schedule_entry): |
|
291 return schedule_entry['trip'].name, schedule_entry['time'] |
|
292 |
|
293 def describe(bus_stop, week_schedule): |
272 schedule = [] |
294 schedule = [] |
273 from collections import defaultdict, Counter |
295 from collections import defaultdict, Counter |
274 from busroute import simplify_name |
296 from busroute import simplify_name |
275 destinations_per_route = defaultdict(Counter) |
297 destinations_per_route = defaultdict(Counter) |
276 def route_key(route_ref): |
298 def route_key(route_ref): |
286 data = [] |
308 data = [] |
287 names = [] |
309 names = [] |
288 from collections import defaultdict |
310 from collections import defaultdict |
289 night_routes = defaultdict(lambda: True) |
311 night_routes = defaultdict(lambda: True) |
290 num_leaves = 0 |
312 num_leaves = 0 |
291 all_routes = Counter() |
313 trip_mapping = {} |
292 for schedule_entry in week_schedule(bus_stop, arrivals = True): #bus_stop.schedule(max_amount = 500, arrivals = True): |
314 for schedule_entry in week_schedule: |
|
315 from busroute import greatly_simplify_name |
293 sign_tuple = tuple(sign_elements(schedule_entry, format = 'short')) |
316 sign_tuple = tuple(sign_elements(schedule_entry, format = 'short')) |
|
317 sign_tuple = tuple(greatly_simplify_name(k) for k in sign_tuple) |
294 route = schedule_entry['trip'].route.reference |
318 route = schedule_entry['trip'].route.reference |
295 night_routes[route] &= is_weekend_night(schedule_entry['time']) |
319 night_routes[route] &= is_weekend_night(schedule_entry['time']) |
296 destinations_per_route[route][sign_tuple] += 1 |
320 destinations_per_route[route][sign_tuple] += 1 |
297 all_routes[route] += 1 |
321 trip_mapping[schedule_entry_hash(schedule_entry)] = (route, sign_tuple) |
298 num_leaves += 1 |
322 num_leaves += 1 |
299 night_routes = {key for key, value in night_routes.items() if value} |
323 night_routes = {key for key, value in night_routes.items() if value} |
300 routes_per_destination = defaultdict(set) |
324 routes_per_destination = defaultdict(set) |
301 for route in destinations_per_route: |
325 all_variants = [] |
302 winner, count = destinations_per_route[route].most_common()[0] |
326 variant_names = {} |
303 if all_routes[route] >= 10 or all_routes[route] / num_leaves >= 0.01: |
327 # Collect all variants |
304 winner = filter_names(winner) |
328 for route, tally in destinations_per_route.items(): |
305 #destinations_per_route[route] = winner and ' - '.join(winner) or '' |
329 variants = variant_abbreviations(tally.keys()) |
306 routes_per_destination[winner].add(route) |
330 winner, count = tally.most_common()[0] |
|
331 for destination, count in tally.items(): |
|
332 route_name = route |
|
333 if destination != winner: |
|
334 route_name += encircle(variants[destination]) |
|
335 if route in night_routes: |
|
336 night_routes.add(route_name) |
|
337 variant_names[route, destination] = route_name |
|
338 all_variants.append({ |
|
339 'name': route_name, |
|
340 'destination': filter_names(destination), |
|
341 'count': count |
|
342 }) |
|
343 all_variants.sort(key = lambda k: k['count']) |
|
344 route_variant_count = len(all_variants) |
|
345 # Only consider variants so that they cover at least 99% of bus leaves |
|
346 coverage = 0 |
|
347 while all_variants: #coverage / num_leaves < 0.99: |
|
348 variant = all_variants.pop() |
|
349 routes_per_destination[variant['destination']].add(variant['name']) |
|
350 coverage += variant['count'] |
307 for key in routes_per_destination: |
351 for key in routes_per_destination: |
308 routes_per_destination[key] = sorted(routes_per_destination[key], key = route_key) |
352 routes_per_destination[key] = sorted(routes_per_destination[key], key = route_key) |
309 def route_len(route): |
353 def route_len(route): |
310 length = 0 |
354 length = 0 |
311 for char in route: |
355 for char in route: |
314 else: |
358 else: |
315 break |
359 break |
316 return length or len(route) |
360 return length or len(route) |
317 from math import inf |
361 from math import inf |
318 def route_key(route): |
362 def route_key(route): |
319 return (route in night_routes, route_len(route), str(route)) |
363 return ( |
|
364 route in night_routes, |
|
365 route_len(route), |
|
366 str(route) |
|
367 ) |
320 def routes_key(routes): |
368 def routes_key(routes): |
321 return min(route_key(route) for route in routes) |
369 return min(route_key(route) for route in routes) |
322 result = [] |
370 result = [] |
323 for regions, routes in sorted( |
371 for regions, routes in sorted( |
324 routes_per_destination.items(), |
372 routes_per_destination.items(), |
329 ' - '.join(tr(region, 'region_short_name') for region in regions) |
377 ' - '.join(tr(region, 'region_short_name') for region in regions) |
330 )) |
378 )) |
331 return { |
379 return { |
332 'night-routes': night_routes, |
380 'night-routes': night_routes, |
333 'all-night-routes': lambda entry, description: all(route in description['night-routes'] for route in entry[0]), |
381 'all-night-routes': lambda entry, description: all(route in description['night-routes'] for route in entry[0]), |
334 'simple': len(all_routes) <= 1, |
382 'simple': route_variant_count <= 1, |
335 'description': result, |
383 'description': result, |
336 'wtf': destinations_per_route, |
384 'wtf': destinations_per_route, |
|
385 'variant-map': {k:variant_names[v] for k, v in trip_mapping.items()}, |
337 } |
386 } |
338 |
387 |
339 @app.route('/stop_description/<reference>') |
388 @app.route('/stop_description/<reference>') |
340 def bus_stop_description(reference): |
389 def bus_stop_description(reference): |
341 from buses import bus_stops |
390 from buses import bus_stops |
342 from pprint import pformat |
391 from pprint import pformat |
343 try: |
392 try: |
344 bus_stop = bus_stops[reference] |
393 bus_stop = bus_stops[reference] |
345 except KeyError: |
394 except KeyError: |
346 abort(404) |
395 abort(404) |
347 return '<pre>' + pformat(describe(bus_stop)) + '</pre>' |
396 return '<pre>' + pformat(describe(bus_stop, week_schedule(bus_stop, arrivals = True))) + '</pre>' |
348 |
397 |
349 @app.route('/api/describe_destination/<trip_reference>/<stop_reference>/<int:index>') |
398 @app.route('/api/describe_destination/<trip_reference>/<stop_reference>/<int:index>') |
350 def describe_destination(trip_reference, stop_reference, index): |
399 def describe_destination(trip_reference, stop_reference, index): |
351 from buses import bus_stops, all_trips |
400 from buses import bus_stops, all_trips |
352 from busroute import simplify_name |
401 from busroute import simplify_name |
574 try: |
623 try: |
575 bus_stop = bus_stops[stop_reference] |
624 bus_stop = bus_stops[stop_reference] |
576 except KeyError: |
625 except KeyError: |
577 abort(404) |
626 abort(404) |
578 week_model = {} |
627 week_model = {} |
579 for schedule_entry in week_schedule(bus_stop, arrivals = True): |
628 bus_stop_schedule = list(week_schedule(bus_stop, arrivals = True)) |
|
629 description = describe(bus_stop, bus_stop_schedule) |
|
630 for schedule_entry in bus_stop_schedule: |
580 route_ref = schedule_entry['trip'].route.reference |
631 route_ref = schedule_entry['trip'].route.reference |
581 if route_filter(route_ref) and dest_filter(schedule_entry['trip']): |
632 if route_filter(route_ref) and dest_filter(schedule_entry['trip']): |
582 time = schedule_entry['time'] |
633 time = schedule_entry['time'] |
583 date = schedule_entry['date'] |
634 date = schedule_entry['date'] |
584 if date not in week_model: |
635 if date not in week_model: |
586 day_model = week_model[date] |
637 day_model = week_model[date] |
587 if time.hour not in day_model: |
638 if time.hour not in day_model: |
588 day_model[time.hour] = [] |
639 day_model[time.hour] = [] |
589 hour_model = day_model[time.hour] |
640 hour_model = day_model[time.hour] |
590 hour_model.append({ |
641 hour_model.append({ |
591 'route': route_ref, |
642 'route': description['variant-map'][schedule_entry_hash(schedule_entry)], |
592 'route-splice': split_route_ref(route_ref), |
643 'route-splice': split_route_ref(route_ref), |
593 'trip': schedule_entry['stop'].trip.name, |
644 'trip': schedule_entry['stop'].trip.name, |
594 'night': is_night_time(schedule_entry['time']), |
645 'night': is_night_time(schedule_entry['time']), |
595 'minute': time.minute, |
646 'minute': time.minute, |
596 }) |
647 }) |