2 from flask import Flask, render_template, abort, send_from_directory, redirect |
2 from flask import Flask, render_template, abort, send_from_directory, redirect |
3 from datetime import datetime, date, time, timedelta |
3 from datetime import datetime, date, time, timedelta |
4 from os import path, listdir, environ |
4 from os import path, listdir, environ |
5 from configparser import ConfigParser |
5 from configparser import ConfigParser |
6 import locale |
6 import locale |
7 |
|
8 app = Flask(__name__) |
7 app = Flask(__name__) |
9 |
|
10 from misc import * |
8 from misc import * |
11 from busroute import reduce_schedule |
9 from busroute import reduce_schedule |
12 from busroute import simplify_name |
|
13 import buses |
10 import buses |
14 |
|
15 regions = {} |
11 regions = {} |
16 suffix_regions = {'naantalin pikatie', 'helsingin valtatie', 'kansanpuisto'} |
12 |
17 |
|
18 # Varmista ettei järjestelmän kieliasetukset sotke muotoiluja |
|
19 def reset_locale(): |
13 def reset_locale(): |
|
14 ''' |
|
15 Resets the locale to system so that the system language settings |
|
16 do not mess with the formatting. |
|
17 ''' |
20 locale.setlocale(locale.LC_ALL, locale.getdefaultlocale()) |
18 locale.setlocale(locale.LC_ALL, locale.getdefaultlocale()) |
21 |
19 |
22 def activate_locale(language = None): |
20 def activate_locale(language = None): |
|
21 ''' |
|
22 Activates either the locale for the provided language, or the default locale. |
|
23 Returns such an object that resets the locale upon function exit. |
|
24 ''' |
23 language = language or language_for_page() |
25 language = language or language_for_page() |
24 class result: |
26 class result: |
25 def __enter__(self): |
27 def __enter__(self): |
26 locale.setlocale(locale.LC_ALL, tr('locale', 'other', language = language)) |
28 if language: |
|
29 locale.setlocale(locale.LC_ALL, tr('locale', 'other', language = language)) |
27 def __exit__(self, *args): |
30 def __exit__(self, *args): |
28 reset_locale() |
31 reset_locale() |
29 return result() |
32 return result() |
30 |
33 |
|
34 def simplify_name(region_name, replace = False): |
|
35 region = regions.get(region_name) |
|
36 if region: |
|
37 if replace and 'replacement' in region: |
|
38 return simplify_name(region['replacement']) |
|
39 return tr(region.get('name', region_name), 'region_short_name', default = region.get('short_name', region_name)) |
|
40 else: |
|
41 return tr(region_name, 'region_short_name') |
|
42 |
31 reset_locale() |
43 reset_locale() |
32 |
44 |
33 # Load translations |
45 # Load translations |
34 class Translator: |
46 class Translator: |
35 def __init__(self): |
47 def __init__(self, languages = None): |
36 self.languages = {} |
48 self.languages = languages or dict() |
37 def load_language(self, file_path): |
49 def load_language(self, file_path): |
38 language_name = path.splitext(path.basename(file_path))[0] |
50 language_name = path.splitext(path.basename(file_path))[0] |
39 ini = ConfigParser() |
51 ini = ConfigParser() |
40 ini.read(path.join(file_path)) |
52 ini.read(path.join(file_path)) |
41 self.languages[language_name] = ini |
53 self.languages[language_name] = ini |
42 def __call__(self, name, *sections, language = None): |
54 def __call__(self, name, *sections, language = None, default = None): |
43 language = language or language_for_page() |
55 language = language or language_for_page() |
44 for section in sections: |
56 for section in sections: |
45 try: |
57 try: |
|
58 print('Trying:', repr(language), repr(section), repr(name)) |
46 return self.languages[language][section][name] |
59 return self.languages[language][section][name] |
47 except KeyError: |
60 except KeyError: |
48 try: |
61 try: |
49 return profile['tr:' + language + ':' + section][name] |
62 return profile['tr:' + language + ':' + section][name] |
50 except KeyError: |
63 except KeyError: |
51 pass |
64 pass |
52 else: |
65 else: |
53 return name[:1].upper() + name[1:] |
66 return default or (name[:1].upper() + name[1:]) |
54 def load_region(self, region): |
67 def load_region(self, region): |
55 for key, value in region.items(): |
68 for key, value in region.items(): |
56 if ':' in key: |
69 if ':' in key: |
57 name_type, language = key.split(':', 1) |
70 name_type, language = key.split(':', 1) |
58 if (name_type.endswith('name') or name_type == 'genitive') and language: |
71 if name_type.endswith('name') and language != '': |
59 section = 'region_' + name_type |
72 section = 'region_' + name_type |
60 if section not in self.languages[language]: |
73 if section not in self.languages[language]: |
61 self.languages[language][section] = {} |
74 self.languages[language][section] = {} |
|
75 print(repr(language), repr(section), repr(region['name']), '=', repr(value)) |
62 self.languages[language][section][region['name']] = value |
76 self.languages[language][section][region['name']] = value |
63 def load_regions(self, regions): |
77 def load_regions(self, regions): |
64 for region in regions.values(): |
78 for region in regions.values(): |
65 self.load_region(region) |
79 self.load_region(region) |
|
80 def __repr__(self): |
|
81 return 'Translator(languages = ' + repr(self.languages) + ')' |
66 |
82 |
67 tr = Translator() |
83 tr = Translator() |
68 for file in listdir('tr'): |
84 for file in listdir('tr'): |
69 tr.load_language(path.join('tr', file)) |
85 tr.load_language(path.join('tr', file)) |
70 |
86 |
71 def language_for_page(): |
87 def language_for_page(): |
|
88 ''' |
|
89 Returns the code of which language to use for the page. |
|
90 ''' |
72 from flask import request |
91 from flask import request |
73 if request.args.get('untranslated') is not None: |
92 if request.args.get('untranslated') is not None: |
74 return None |
93 return None |
75 else: |
94 else: |
76 for language_name in tr.languages: |
95 for language_name in tr.languages: |
91 trip_length = trip_length, |
113 trip_length = trip_length, |
92 format = format |
114 format = format |
93 ) |
115 ) |
94 ] |
116 ] |
95 |
117 |
96 def genitive(name): |
|
97 from busroute import regions |
|
98 region = regions.get(name) |
|
99 if region: |
|
100 return region.get('genitive:fi', simplify_name(name) + 'n') |
|
101 else: |
|
102 return simplify_name(name) + 'n' |
|
103 |
|
104 def via_fi(via): |
|
105 if len(via) > 1: |
|
106 return ', '.join(via[:-1]) + ' ja ' + via[-1] |
|
107 else: |
|
108 return via[0] |
|
109 |
|
110 def sign(schedule_entry, format = 'medium'): |
118 def sign(schedule_entry, format = 'medium'): |
|
119 ''' |
|
120 For an entry in a bus stop schedule, find out where the connection is leading to. |
|
121 Returns a string. |
|
122 ''' |
111 sign = sign_elements(schedule_entry, format = format) |
123 sign = sign_elements(schedule_entry, format = format) |
112 if sign: |
124 if sign: |
113 #if language_for_page() == 'fi': |
125 return ' - '.join(tr(simplify_name(place), 'region_name', 'region_short_name') for place in sign) |
114 # if len(sign) > 1: |
|
115 # return simplify_name(sign[-1]) + ' ' + via_fi([genitive(place) for place in sign[:-1]]) + ' kautta' |
|
116 # else: |
|
117 # return simplify_name(sign[0]) |
|
118 # sign_representation = ' - '.join(tr(place, 'region_short_name') for place in sign if place not in suffix_regions) |
|
119 # sign_representation += ''.join(' ' + tr(place, 'suffix-places') for place in sign if place in suffix_regions) |
|
120 # return sign_representation |
|
121 return ' - '.join(tr(simplify_name(place), 'region_short_name') for place in sign) |
|
122 else: |
126 else: |
123 return schedule_entry['trip'].schedule[-1].stop.name |
127 return schedule_entry['trip'].schedule[-1].stop.name |
124 |
128 |
125 def long_form_sign(schedule_entry, format = 'long'): |
129 def long_form_sign(schedule_entry, format = 'long'): |
126 from math import ceil |
130 from math import ceil |
194 schedule = [] |
198 schedule = [] |
195 try: |
199 try: |
196 bus_stop = bus_stops[reference] |
200 bus_stop = bus_stops[reference] |
197 except KeyError: |
201 except KeyError: |
198 abort(404) |
202 abort(404) |
199 for schedule_entry in bus_stop.schedule(max_amount = 100, arrivals = True): |
203 for schedule_entry in bus_stop.schedule(max_amount = 100, max_past = 4, arrivals = True): |
200 route_ref = schedule_entry['trip'].route.reference |
204 route_ref = schedule_entry['trip'].route.reference |
201 schedule.append({ |
205 schedule.append({ |
202 'time': time_representation(schedule_entry['time']), |
206 'time': time_representation(schedule_entry['time']), |
|
207 'timestamp': int(schedule_entry['time'].timestamp()), |
203 'route': route_ref, |
208 'route': route_ref, |
204 'route-splice': split_route_ref(route_ref), |
209 'route-splice': split_route_ref(route_ref), |
205 'sign': sign(schedule_entry), |
210 'sign': sign(schedule_entry), |
206 'trip': schedule_entry['stop'].trip.name, |
211 'trip': schedule_entry['stop'].trip.name, |
207 'night': is_night_time(schedule_entry['time']), |
212 'night': is_night_time(schedule_entry['time']), |
208 'imminent': imminent(schedule_entry), |
213 'imminent': imminent(schedule_entry), |
|
214 'id': str(schedule_entry['stop'].uuid), |
|
215 'gone': schedule_entry['gone'], |
209 }) |
216 }) |
210 return render_template( |
217 return render_template( |
211 'stop.html', |
218 'stop.html', |
212 schedule = schedule, |
219 schedule = schedule, |
213 ref = bus_stop.code, |
220 ref = bus_stop.code, |
349 'destination': filter_names(destination), |
353 'destination': filter_names(destination), |
350 'count': count |
354 'count': count |
351 }) |
355 }) |
352 counts_per_variant[route_name] = count |
356 counts_per_variant[route_name] = count |
353 all_variants.sort(key = lambda k: k['count']) |
357 all_variants.sort(key = lambda k: k['count']) |
|
358 def route_limit(route): |
|
359 return (route in night_routes) and 6 or 20 |
|
360 rare_variants = {variant['name'] for variant in all_variants if variant['count'] < route_limit(variant['name'])} |
354 route_variant_count = len(all_variants) |
361 route_variant_count = len(all_variants) |
355 # Only consider variants so that they cover at least 99% of bus leaves |
|
356 coverage = 0 |
|
357 #while coverage / num_leaves < 0.99: |
|
358 # variant = all_variants.pop() |
|
359 for variant in all_variants: |
362 for variant in all_variants: |
360 routes_per_destination[variant['destination']].add(variant['name']) |
363 routes_per_destination[variant['destination']].add(variant['name']) |
361 coverage += variant['count'] |
|
362 for key in routes_per_destination: |
364 for key in routes_per_destination: |
363 routes_per_destination[key] = sorted(routes_per_destination[key], key = route_key) |
365 routes_per_destination[key] = sorted(routes_per_destination[key], key = route_key) |
364 def route_len(route): |
366 def route_len(route): |
365 length = 0 |
367 length = 0 |
366 for char in route: |
368 for char in route: |
368 length += 1 |
370 length += 1 |
369 else: |
371 else: |
370 break |
372 break |
371 return length or len(route) |
373 return length or len(route) |
372 from math import inf |
374 from math import inf |
373 def route_limit(route): |
|
374 return (route in night_routes) and 6 or 20 |
|
375 def route_key(route): |
375 def route_key(route): |
376 from math import log |
376 from math import log |
377 return ( |
377 return ( |
|
378 counts_per_variant.get(route, 0) < route_limit(route), |
378 route in night_routes, |
379 route in night_routes, |
379 counts_per_variant.get(route, 0) < route_limit(route), |
|
380 route_len(route), |
380 route_len(route), |
381 str(route) |
381 str(route) |
382 ) |
382 ) |
383 def routes_key(routes): |
383 def routes_key(routes): |
384 return min(route_key(route) for route in routes) |
384 return min(route_key(route) for route in routes) |
|
385 # Convert routes per destination to item pairs so that we can split it into rare and non-rare |
|
386 route_destination_pairs = list() |
|
387 for regions, routes in routes_per_destination.items(): |
|
388 common_routes = set(route for route in routes if counts_per_variant[route] >= route_limit(route)) |
|
389 rare_routes = set(routes) - common_routes |
|
390 if common_routes: |
|
391 route_destination_pairs.append((regions, common_routes)) |
|
392 if rare_routes: |
|
393 route_destination_pairs.append((regions, rare_routes)) |
385 result = [] |
394 result = [] |
386 rare_variants = {variant['name'] for variant in all_variants if variant['count'] < route_limit(variant['name'])} |
|
387 rare_variant_groups = set() |
395 rare_variant_groups = set() |
388 for regions, routes in sorted( |
396 for regions, routes in sorted(route_destination_pairs, key = lambda pair: routes_key(pair[1])): |
389 routes_per_destination.items(), |
|
390 key = lambda pair: routes_key(pair[1]) |
|
391 ): |
|
392 routes_tuple = tuple(condense_route_list(sorted(routes, key = route_key))) |
397 routes_tuple = tuple(condense_route_list(sorted(routes, key = route_key))) |
393 result.append(( |
398 result.append(( |
394 routes_tuple, |
399 routes_tuple, |
395 ' - '.join(tr(region, 'region_short_name') for region in regions) |
400 ' - '.join(tr(region, 'region_short_name') for region in regions) |
396 )) |
401 )) |
443 abort(404) |
447 abort(404) |
444 return jsonify({ |
448 return jsonify({ |
445 'abbreviation': trip_abbreviation(trip), |
449 'abbreviation': trip_abbreviation(trip), |
446 }) |
450 }) |
447 |
451 |
|
452 |
|
453 def service_start_time(): |
|
454 from datetime import date, datetime, timedelta |
|
455 result = datetime.now().replace(hour = 0, minute = 0, second = 0, microsecond = 0) |
|
456 if datetime.now().hour < 4: |
|
457 result -= timedelta(1) |
|
458 return result |
|
459 |
|
460 @app.route('/find_halt/<stop_reference>/<blockref>/<int:originalaimeddeparturetime>') |
|
461 def find_halt(stop_reference, blockref, originalaimeddeparturetime): |
|
462 from datetime import datetime |
|
463 from flask import jsonify |
|
464 info = (blockref, datetime.fromtimestamp(originalaimeddeparturetime) - service_start_time()) |
|
465 trip = buses.trips_by_vehicle_info[info] |
|
466 try: |
|
467 return jsonify({ |
|
468 'id': [str(halt.uuid) for halt in buses.trips_by_vehicle_info[info].schedule if halt.stop.reference == stop_reference][0], |
|
469 }) |
|
470 except: |
|
471 abort(404) |
|
472 |
|
473 |
448 def current_bus_day(): |
474 def current_bus_day(): |
449 from datetime import date, datetime, timedelta |
475 from datetime import date, datetime, timedelta |
450 day = date.today() |
476 day = date.today() |
451 if datetime.now().hour < 5: |
477 if datetime.now().hour < 5: |
452 day -= timedelta(1) |
478 day -= timedelta(1) |
462 abort(404) |
488 abort(404) |
463 for i, schedule_entry in enumerate(bus_stop.schedule_for_day(current_bus_day(), arrivals = False)): |
489 for i, schedule_entry in enumerate(bus_stop.schedule_for_day(current_bus_day(), arrivals = False)): |
464 schedule.append({ |
490 schedule.append({ |
465 'time_data': schedule_entry['time'], |
491 'time_data': schedule_entry['time'], |
466 'time': time_representation(schedule_entry['time']), |
492 'time': time_representation(schedule_entry['time']), |
|
493 'timestamp': int(schedule_entry['time'].timestamp()), |
467 'route': schedule_entry['trip'].route.reference, |
494 'route': schedule_entry['trip'].route.reference, |
468 'sign': long_form_sign(schedule_entry, format = 'medium'), |
495 'sign': long_form_sign(schedule_entry, format = 'medium'), |
469 'trip': schedule_entry['stop'].trip.name, |
496 'trip': schedule_entry['stop'].trip.name, |
470 'night': is_night_time(schedule_entry['time']), |
497 'night': is_night_time(schedule_entry['time']), |
471 'imminent': imminent(schedule_entry), |
498 'imminent': imminent(schedule_entry), |
499 cluster = bus_stop.cluster.url_name if len(bus_stop.cluster.stops) > 1 else None, |
526 cluster = bus_stop.cluster.url_name if len(bus_stop.cluster.stops) > 1 else None, |
500 num_imminent_leaves = num_imminent_leaves, |
527 num_imminent_leaves = num_imminent_leaves, |
501 tr = tr, |
528 tr = tr, |
502 ) |
529 ) |
503 |
530 |
504 @app.route('/test') |
|
505 def test(): |
|
506 from buses import bus_stops |
|
507 bus_stop = bus_stops['16'] |
|
508 schedule = [{'imminent': True, |
|
509 'index': 0, |
|
510 'night': False, |
|
511 'route': '2A', |
|
512 'sign': {'destination': 'Kohmo', 'via': ['Nummenmäki', 'Kurala']}, |
|
513 'time': '1m', |
|
514 'trip': '00012501__3798generatedBlock'}, |
|
515 {'imminent': True, |
|
516 'index': 1, |
|
517 'night': False, |
|
518 'route': '54', |
|
519 'sign': {'destination': 'Ylioppilaskylä', 'via': []}, |
|
520 'time': '2m', |
|
521 'trip': '00014359__5656generatedBlock'}, |
|
522 {'imminent': True, |
|
523 'index': 2, |
|
524 'night': False, |
|
525 'route': '1', |
|
526 'sign': {'destination': 'Lentoasema ✈', 'via': ['Urusvuori']}, |
|
527 'time': '3m', |
|
528 'trip': '00010281__1281generatedBlock'}, |
|
529 {'imminent': False, |
|
530 'index': 3, |
|
531 'night': False, |
|
532 'route': '56', |
|
533 'sign': {'destination': 'Räntämäki', 'via': ['Nummenmäki', 'Halinen']}, |
|
534 'time': '8m', |
|
535 'trip': '00014686__5983generatedBlock'}, |
|
536 {'imminent': False, |
|
537 'index': 4, |
|
538 'night': False, |
|
539 'route': '42', |
|
540 'sign': {'destination': 'Varissuo', 'via': ['Kupittaa as', 'Itäharju']}, |
|
541 'time': '18:30', |
|
542 'trip': '00014010__5307generatedBlock'}, |
|
543 {'imminent': False, |
|
544 'index': 5, |
|
545 'night': False, |
|
546 'route': '2B', |
|
547 'sign': {'destination': 'Littoinen', |
|
548 'via': ['Nummenmäki', 'Kurala', 'Kohmo']}, |
|
549 'time': '18:35', |
|
550 'trip': '00012629__3926generatedBlock'}] |
|
551 return render_template( |
|
552 'stop_display.html', |
|
553 schedule = schedule, |
|
554 ref = bus_stop.code, |
|
555 name = tr(bus_stop.name, 'bus-stops'), |
|
556 link_to_map = bus_stop.location.link_to_map, |
|
557 region = bus_stop.region, |
|
558 location = bus_stop.location, |
|
559 cluster = bus_stop.cluster.url_name if len(bus_stop.cluster.stops) > 1 else None, |
|
560 num_imminent_leaves = max(1, sum(schedule_entry['imminent'] for schedule_entry in schedule)), |
|
561 tr = tr, |
|
562 ) |
|
563 |
|
564 def time_representation(time, relative = True): |
531 def time_representation(time, relative = True): |
565 time_difference = time - now() |
532 time_difference = time - now() |
566 if relative and timedelta(minutes = -1) < time_difference < timedelta(minutes = 1): |
533 #if relative and timedelta(minutes = -1) < time_difference < timedelta(minutes = 1): |
567 return tr('right-now', 'misc-text') |
534 # return tr('right-now', 'misc-text') |
568 elif relative and time_difference > timedelta(0) and time_difference < timedelta(minutes = 10): |
535 #elif relative and time_difference > timedelta(0) and time_difference < timedelta(minutes = 10): |
569 return '%dm' % round(time_difference.seconds / 60) |
536 # return '%dm' % round(time_difference.seconds / 60) |
570 elif time.date() == today(): |
537 if time.date() == today(): |
571 return '%d:%02d' % (time.hour, time.minute) |
538 return '%d:%02d' % (time.hour, time.minute) |
572 elif time_difference < timedelta(7): |
539 elif time_difference < timedelta(7): |
573 with activate_locale(): |
540 with activate_locale(): |
574 return time.strftime('%-a %H:%M').replace(' ', '\xa0') |
541 return time.strftime('%-a %H:%M').replace(' ', '\xa0') |
575 else: |
542 else: |