service.py

changeset 127
2bc0529d44a5
parent 114
b736478416d4
child 134
4ac0f2e2ec4e
equal deleted inserted replaced
126:369e242edc5d 127:2bc0529d44a5
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:
78 return language_name 97 return language_name
79 else: 98 else:
80 return request.accept_languages.best_match(tr.languages) 99 return request.accept_languages.best_match(tr.languages)
81 100
82 def sign_elements(schedule_entry, format = 'medium'): 101 def sign_elements(schedule_entry, format = 'medium'):
102 '''
103 For an entry in a bus stop schedule, find out where the connection is leading to.
104 Returns a list of places, possibly empty.
105 '''
83 from math import ceil 106 from math import ceil
84 from busroute import simplify_name
85 trip_length = schedule_entry['trip'].length - schedule_entry['stop'].traveled_distance 107 trip_length = schedule_entry['trip'].length - schedule_entry['stop'].traveled_distance
86 regions = schedule_entry['trip'].concise_schedule(schedule_entry['stop']) 108 regions = schedule_entry['trip'].concise_schedule(schedule_entry['stop'])
87 return [ 109 return [
88 name 110 name
89 for name in reduce_schedule( 111 for name in reduce_schedule(
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,
271 def is_weekend_night(time): 278 def is_weekend_night(time):
272 from datetime import timedelta 279 from datetime import timedelta
273 adjusted_time = time - timedelta(hours = 4, minutes = 30) 280 adjusted_time = time - timedelta(hours = 4, minutes = 30)
274 return adjusted_time.weekday() in [4, 5] and is_night_time(time) 281 return adjusted_time.weekday() in [4, 5] and is_night_time(time)
275 282
276 #encircled = '\u24b6\u24b7\u24b8\u24b9\u24ba\u24bb\u24bc\u24bd\u24be\u24bf' \ 283 encircled = '⒜⒝⒞⒟⒠⒡⒢⒣⒤⒥⒦⒧⒨⒩⒪⒫⒬⒭⒮⒯⒰⒱⒲⒳⒴⒵'
277 # '\u24c0\u24c1\u24c2\u24c3\u24c4\u24c5\u24c6\u24c7\u24c8\u24c9\u24ca\u24cb' \ 284 #encircled = 'ⓐⓑⓒⓓⓔⓕⓖⓗⓘⓙⓚⓛⓜⓝⓞⓟⓠⓡⓢⓣⓤⓥⓦⓧⓨⓩ'
278 # '\u24cc\u24cd\u24ce\u24cf'
279 #encircled = '⒜⒝⒞⒟⒠⒡⒢⒣⒤⒥⒦⒧⒨⒩⒪⒫⒬⒭⒮⒯⒰⒱⒲⒳⒴⒵'
280 encircled = 'ⓐⓑⓒⓓⓔⓕⓖⓗⓘⓙⓚⓛⓜⓝⓞⓟⓠⓡⓢⓣⓤⓥⓦⓧⓨⓩ'
281 285
282 def encircle(char): 286 def encircle(char):
283 from string import ascii_uppercase 287 from string import ascii_uppercase
284 try: 288 try:
285 return encircled[ascii_uppercase.index(char.upper())] 289 return encircled[ascii_uppercase.index(char.upper())]
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 ))
399 return { 404 return {
400 'night-routes': night_routes, 405 'night-routes': night_routes,
401 'all-night-routes': lambda entry, description: all(route in description['night-routes'] for route in entry[0]), 406 'all-night-routes': lambda entry, description: all(route in description['night-routes'] for route in entry[0]),
402 'simple': route_variant_count <= 1, 407 'simple': route_variant_count <= 1,
403 'description': result, 408 'description': result,
404 'wtf': destinations_per_route,
405 'variant-map': {k:variant_names[v] for k, v in trip_mapping.items()}, 409 'variant-map': {k:variant_names[v] for k, v in trip_mapping.items()},
406 'rare-variants': rare_variants, 410 'rare-variants': rare_variants,
407 'rare-variant-groups': rare_variant_groups, 411 'rare-variant-groups': rare_variant_groups,
408 } 412 }
409 413
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:
619 except KeyError: 586 except KeyError:
620 abort(404) 587 abort(404)
621 else: 588 else:
622 return make_cluster(cluster) 589 return make_cluster(cluster)
623 590
624 @app.route('/custom') 591 @app.route('/cluster')
625 def custom_cluster(): 592 def custom_cluster():
626 from flask import request 593 from flask import request
627 from buses import bus_stops, CustomBusStopCluster 594 from buses import bus_stops, CustomBusStopCluster
628 if 'stops' in request.args: 595 if 'stops' in request.args:
629 cluster = CustomBusStopCluster( 596 cluster = CustomBusStopCluster(
921 'interesting.html', 888 'interesting.html',
922 data = data, 889 data = data,
923 tr = tr, 890 tr = tr,
924 ) 891 )
925 892
926 @app.route('/')
927 def index():
928 return redirect('stop_cluster/kauppatori')
929
930 @app.route('/pysäkki/<reference>')
931 def redirect_pysäkki(reference):
932 return redirect('stop/' + str(reference))
933
934 @app.route('/pysäkkiryhmä/<reference>')
935 def redirect_pysäkkiryhmä(reference):
936 return redirect('stop_cluster/' + str(reference))
937
938 @app.route('/ajovuoro/<reference>')
939 def redirect_ajovuoro(reference):
940 return redirect('trip/' + str(reference))
941
942 @app.route('/static/<path:path>') 893 @app.route('/static/<path:path>')
943 def static_file(path): 894 def static_file(path):
944 return send_from_directory(path.join('static', path)) 895 return send_from_directory(path.join('static', path))
945 896
946 from argparse import ArgumentParser 897 from argparse import ArgumentParser

mercurial