8 |
8 |
9 def transform_trip_reference(reference): |
9 def transform_trip_reference(reference): |
10 return reference |
10 return reference |
11 |
11 |
12 class BusTrip: |
12 class BusTrip: |
13 def __init__(self, reference, route, service, length): |
13 def __init__(self, reference, route, service, length, block_id): |
14 self.reference, self.route, self.service = reference, route, service |
14 self.reference, self.route, self.service, self.block_id = reference, route, service, block_id |
15 self.length = length |
15 self.length = length |
16 self.schedule = [] |
16 self.schedule = [] |
17 self.name = transform_trip_reference(reference) |
17 self.name = transform_trip_reference(reference) |
18 def __repr__(self): |
18 def __repr__(self): |
19 return 'all_trips[%r]' % self.name |
19 return 'all_trips[%r]' % self.name |
27 try: |
27 try: |
28 return self.service in services_for_day[day] |
28 return self.service in services_for_day[day] |
29 except KeyError: |
29 except KeyError: |
30 return False |
30 return False |
31 def concise_schedule(self, starting_stop = None): |
31 def concise_schedule(self, starting_stop = None): |
32 if starting_stop and starting_stop in self.schedule: |
32 if profile['regions']['use-regions']: |
33 schedule = copy(self.schedule) |
33 if starting_stop and starting_stop in self.schedule: |
34 schedule = schedule[schedule.index(starting_stop):] |
34 schedule = copy(self.schedule) |
|
35 schedule = schedule[schedule.index(starting_stop):] |
|
36 else: |
|
37 schedule = self.schedule |
|
38 used_areas = set() |
|
39 result = [] |
|
40 for halt in schedule: |
|
41 stop = halt.stop |
|
42 if stop.region and stop.region not in used_areas: |
|
43 used_areas.add(stop.region) |
|
44 result.append(stop.region) |
|
45 return result |
35 else: |
46 else: |
36 schedule = self.schedule |
47 return [halt.stop.name for halt in self.schedule] |
37 used_areas = set() |
|
38 result = [] |
|
39 for halt in schedule: |
|
40 stop = halt.stop |
|
41 if stop.region and stop.region not in used_areas: |
|
42 used_areas.add(stop.region) |
|
43 result.append(stop.region) |
|
44 return result |
|
45 |
48 |
46 class BusRoute: |
49 class BusRoute: |
47 def __init__(self, entry): |
50 def __init__(self, entry): |
48 self.id = entry['route_id'] |
51 self.id = entry['route_id'] |
49 self.reference = entry['route_short_name'] |
52 self.reference = entry['route_short_name'] |
128 self.arrival_time, self.departure_time, self.stop, self.trip = arrival_time, departure_time, \ |
131 self.arrival_time, self.departure_time, self.stop, self.trip = arrival_time, departure_time, \ |
129 stop, trip |
132 stop, trip |
130 self.traveled_distance = traveled_distance |
133 self.traveled_distance = traveled_distance |
131 @property |
134 @property |
132 def is_arrival(self): |
135 def is_arrival(self): |
133 if not hasattr(self, 'cachedIsArrival'): |
136 if profile['regions']['use-regions']: |
134 if self.stop.region: |
137 if not hasattr(self, 'cachedIsArrival'): |
135 iterator = iter(self.trip.schedule) |
138 if self.stop.region: |
136 stop = next(iterator) |
139 iterator = iter(self.trip.schedule) |
137 while stop is not self: |
|
138 stop = next(iterator) |
140 stop = next(iterator) |
139 for stop in iterator: |
141 while stop is not self: |
140 if stop.stop.region != self.stop.region: |
142 stop = next(iterator) |
141 self.cachedIsArrival = False |
143 for stop in iterator: |
142 break |
144 if stop.stop.region != self.stop.region: |
|
145 self.cachedIsArrival = False |
|
146 break |
|
147 else: |
|
148 self.cachedIsArrival = True |
143 else: |
149 else: |
144 self.cachedIsArrival = True |
150 self.cachedIsArrival = False |
145 else: |
151 return self.cachedIsArrival |
146 self.cachedIsArrival = False |
152 else: |
147 return self.cachedIsArrival |
153 return self == self.trip.schedule[-1] |
148 def __repr__(self): |
154 def __repr__(self): |
149 return 'BusHalt(%r, %r, %r, %r)' % (self.arrival_time, self.departure_time, self.stop, self.trip) |
155 return 'BusHalt(%r, %r, %r, %r)' % (self.arrival_time, self.departure_time, self.stop, self.trip) |
|
156 def sign(self, long = False): |
|
157 from busroute import reduce_schedule |
|
158 return reduce_schedule( |
|
159 route = self.trip.concise_schedule(self), |
|
160 trip_length = self.trip.length - self.traveled_distance, |
|
161 long = long, |
|
162 ) |
150 |
163 |
151 routes = {} |
164 routes = {} |
152 routes_per_id = {} |
165 routes_per_id = {} |
153 all_trips = {} |
166 all_trips = {} |
154 services = {} |
167 services = {} |
156 all_clusters = set() |
169 all_clusters = set() |
157 viimeinen_käyttöpäivä = None |
170 viimeinen_käyttöpäivä = None |
158 clusters_by_name = {} |
171 clusters_by_name = {} |
159 services_for_day = {} |
172 services_for_day = {} |
160 |
173 |
161 def load_buses(gtfs_zip_path, profile): |
174 def load_buses(gtfs_zip_path): |
162 global viimeinen_käyttöpäivä |
175 global viimeinen_käyttöpäivä |
163 from zipfile import ZipFile |
176 from zipfile import ZipFile |
164 with ZipFile(gtfs_zip_path) as gtfs_zip: |
177 with ZipFile(gtfs_zip_path) as gtfs_zip: |
165 print('Ladataan linjat... ', file = stderr, end = '', flush = True) |
178 print('Ladataan linjat... ', file = stderr, end = '', flush = True) |
166 with gtfs_zip.open('routes.txt') as file: |
179 with gtfs_zip.open('routes.txt') as file: |
171 print('%d linjaa' % len(routes), file = stderr) |
184 print('%d linjaa' % len(routes), file = stderr) |
172 |
185 |
173 print('Ladataan ajovuorot... ', file = stderr, end = '', flush = True) |
186 print('Ladataan ajovuorot... ', file = stderr, end = '', flush = True) |
174 |
187 |
175 shape_distances = {} |
188 shape_distances = {} |
176 with gtfs_zip.open('shapes.txt') as file: |
189 try: |
177 for row in read_csv(map(bytes.decode, file)): |
190 with gtfs_zip.open('shapes.txt') as file: |
178 shape_distances[row['shape_id']] = max(shape_distances.get(row['shape_id'], 0), float(row['shape_dist_traveled'])) |
191 for row in read_csv(map(bytes.decode, file)): |
|
192 shape_distances[row['shape_id']] = max(shape_distances.get(row['shape_id'], 0), float(row['shape_dist_traveled'])) |
|
193 except KeyError: |
|
194 pass |
179 |
195 |
180 with gtfs_zip.open('trips.txt') as file: |
196 with gtfs_zip.open('trips.txt') as file: |
181 for row in read_csv(map(bytes.decode, file)): |
197 for row in read_csv(map(bytes.decode, file)): |
182 if row['service_id'] not in services: |
198 if row['service_id'] not in services: |
183 services[row['service_id']] = BusService(row['service_id']) |
199 services[row['service_id']] = BusService(row['service_id']) |
184 route = routes_per_id[row['route_id']] |
200 route = routes_per_id[row['route_id']] |
185 trip = BusTrip( |
201 trip = BusTrip( |
186 reference = row['trip_id'], |
202 reference = row['trip_id'], |
187 route = route, |
203 route = route, |
188 service = services[row['service_id']], |
204 service = services[row['service_id']], |
189 length = shape_distances[row['shape_id']] * float(profile['metrics']['shape-modifier']) |
205 length = shape_distances.get(row.get('shape_id'), 1) * float(profile['metrics']['shape-modifier']), |
|
206 block_id = row['block_id'], |
190 ) |
207 ) |
191 route.trips.add(trip) |
208 route.trips.add(trip) |
192 assert trip.name not in all_trips |
209 if trip.name in all_trips: |
193 all_trips[trip.name] = trip |
210 print('Trip %s already exists' % trip.name) |
|
211 else: |
|
212 all_trips[trip.name] = trip |
194 print('%d ajoa' % len(all_trips), file = stderr) |
213 print('%d ajoa' % len(all_trips), file = stderr) |
195 |
214 |
196 def read_date(teksti): |
215 def read_date(teksti): |
197 return date(int(teksti[:4]), int(teksti[4:6]), int(teksti[6:])) |
216 return date(int(teksti[:4]), int(teksti[4:6]), int(teksti[6:])) |
198 |
217 |
251 location = Sijainti(float(row['stop_lat']), float(row['stop_lon'])) |
270 location = Sijainti(float(row['stop_lat']), float(row['stop_lon'])) |
252 stop = BusStop( |
271 stop = BusStop( |
253 reference = row['stop_id'], |
272 reference = row['stop_id'], |
254 name = row['stop_name'], |
273 name = row['stop_name'], |
255 location = location, |
274 location = location, |
256 code = row['stop_code'], |
275 code = row.get('stop_code', row['stop_id']), |
257 ) |
276 ) |
258 bus_stops[stop.reference] = stop |
277 bus_stops[stop.reference] = stop |
259 with open('regions-per-stop.json') as file: |
278 if profile['regions']['use-regions']: |
260 for stop_reference, region in json.load(file).items(): |
279 with open('regions-per-stop.json') as file: |
261 bus_stops[stop_reference].region = region |
280 for stop_reference, region in json.load(file).items(): |
|
281 try: |
|
282 bus_stops[stop_reference].region = region |
|
283 except KeyError: |
|
284 pass |
262 print('%d pysäkkiä' % len(bus_stops), file = stderr) |
285 print('%d pysäkkiä' % len(bus_stops), file = stderr) |
263 |
286 |
264 |
287 |
265 class BusStopCluster: |
288 class BusStopCluster: |
266 def __init__(self): |
289 def __init__(self): |
365 for name, clusters in clusters_per_name.items(): |
388 for name, clusters in clusters_per_name.items(): |
366 if len(clusters) == 1: |
389 if len(clusters) == 1: |
367 # Ryhmä on ainoa jolla on varaus tälle nimelle. Sen kuin vaan. |
390 # Ryhmä on ainoa jolla on varaus tälle nimelle. Sen kuin vaan. |
368 next(iter(clusters)).name = name |
391 next(iter(clusters)).name = name |
369 else: |
392 else: |
370 # Olisiko kaikki klusterit eri alueilla? |
393 if profile['regions']['use-regions']: |
371 common_regions = shared_elements_in_n_sets({stop.region for stop in cluster.stops} for cluster in clusters) |
394 # Olisiko kaikki klusterit eri alueilla? |
372 # Esitys: ryhmä -> ne alueet jotka ovat tälle ryhmälle ainutlaatuisia |
395 common_regions = shared_elements_in_n_sets({stop.region for stop in cluster.stops} for cluster in clusters) |
373 proposal = { |
396 # Esitys: ryhmä -> ne alueet jotka ovat tälle ryhmälle ainutlaatuisia |
374 cluster: {stop.region for stop in cluster.stops} - common_regions - {None} |
397 proposal = { |
|
398 cluster: {stop.region for stop in cluster.stops} - common_regions - {None} |
|
399 for cluster in clusters |
|
400 } |
|
401 # Jos enintään yksi klusteri tässä esityksessä on kokonaan ilman omaa aluetta, jolla se voisi eritellä, |
|
402 # niin nimetään klusterit näiden alueiden mukaan. |
|
403 # Se klusteri jolla ei ole omaa aluetta (jos on) jätetään ilman aluepäätettä. |
|
404 if sum([1 for unique_areas in proposal.values() if not unique_areas]) <= 1: |
|
405 for cluster, unique_areas in proposal.items(): |
|
406 individual_cluster_name = name |
|
407 if unique_areas: |
|
408 individual_cluster_name += ' (' + min(unique_areas) + ')' |
|
409 cluster.name = individual_cluster_name |
|
410 break |
|
411 # Typerä reunatapaus. Indeksoidaan numeroin... |
|
412 for n, (_, cluster) in enumerate(sorted( |
|
413 min((stop.reference.lower(), cluster) for stop in cluster.stops) |
375 for cluster in clusters |
414 for cluster in clusters |
376 } |
415 ), 1): |
377 # Jos enintään yksi klusteri tässä esityksessä on kokonaan ilman omaa aluetta, jolla se voisi eritellä, |
416 individual_cluster_name = name + '-' + str(n) |
378 # niin nimetään klusterit näiden alueiden mukaan. |
417 cluster.name = individual_cluster_name |
379 # Se klusteri jolla ei ole omaa aluetta (jos on) jätetään ilman aluepäätettä. |
|
380 if sum([1 for unique_areas in proposal.values() if not unique_areas]) <= 1: |
|
381 for cluster, unique_areas in proposal.items(): |
|
382 individual_cluster_name = name |
|
383 if unique_areas: |
|
384 individual_cluster_name += ' (' + min(unique_areas) + ')' |
|
385 cluster.name = individual_cluster_name |
|
386 else: |
|
387 # Typerä reunatapaus. Indeksoidaan numeroin... |
|
388 for n, (_, cluster) in enumerate(sorted( |
|
389 min((stop.reference.lower(), cluster) for stop in cluster.stops) |
|
390 for cluster in clusters |
|
391 ), 1): |
|
392 individual_cluster_name = name + '-' + str(n) |
|
393 cluster.name = individual_cluster_name |
|
394 |
418 |
395 print('Ryhmitellään pysäkit...') |
419 print('Ryhmitellään pysäkit...') |
396 cluster_bus_stops() |
420 cluster_bus_stops() |
397 name_clusters() |
421 name_clusters() |
398 |
422 |
406 with gtfs_zip.open('stop_times.txt') as file: |
430 with gtfs_zip.open('stop_times.txt') as file: |
407 row_count = sum(line.count(b'\n') for line in file) |
431 row_count = sum(line.count(b'\n') for line in file) |
408 with gtfs_zip.open('stop_times.txt') as file: |
432 with gtfs_zip.open('stop_times.txt') as file: |
409 progress = 0 |
433 progress = 0 |
410 for row in read_csv(map(bytes.decode, file)): |
434 for row in read_csv(map(bytes.decode, file)): |
|
435 if int(row.get('pickup_type', 0)) and int(row.get('drop_off_type', 0)): |
|
436 continue |
411 trip = all_trips[transform_trip_reference(row['trip_id'])] |
437 trip = all_trips[transform_trip_reference(row['trip_id'])] |
412 arrival_time = read_time(row['arrival_time']) |
438 arrival_time = read_time(row['arrival_time']) |
413 departure_time = read_time(row['departure_time']) |
439 departure_time = read_time(row['departure_time']) |
414 stop = bus_stops[row['stop_id']] |
440 stop = bus_stops[row['stop_id']] |
415 traveled_distance = float(row['shape_dist_traveled']) * float(profile['metrics']['shape-modifier']) |
441 traveled_distance = float(row.get('shape_dist_traveled', 1)) * float(profile['metrics']['shape-modifier']) |
416 trip.schedule.append(BusHalt(arrival_time, departure_time, stop, trip, traveled_distance)) |
442 trip.schedule.append(BusHalt(arrival_time, departure_time, stop, trip, traveled_distance)) |
417 stop.involved_trips.add(trip) |
443 stop.involved_trips.add(trip) |
418 progress += 1 |
444 progress += 1 |
419 if progress % 1000 == 0: |
445 if progress % 1000 == 0: |
420 print('\rLadataan aikataulut... %.1f%%' % (progress * 100 / row_count), end = ' ', file = stderr) |
446 print('\rLadataan aikataulut... %.1f%%' % (progress * 100 / row_count), end = ' ', file = stderr) |
448 try: |
474 try: |
449 most_common_route = tally.most_common(1)[0][0] |
475 most_common_route = tally.most_common(1)[0][0] |
450 route.description = simplify_name(most_common_route[0]) + ' - ' + simplify_name(most_common_route[-1]) |
476 route.description = simplify_name(most_common_route[0]) + ' - ' + simplify_name(most_common_route[-1]) |
451 except: |
477 except: |
452 route.description = '' |
478 route.description = '' |
453 route.trips = sorted(route.trips, key = lambda trip: trip.schedule[0].departure_time) |
479 route.trips = sorted(route.trips, key = lambda trip: trip.schedule and trip.schedule[0].departure_time or timedelta()) |
454 |
480 |
455 # Fölin datassa on jotain tosi kummaa. Ilmeisesti ajovuoron viimeisen pysähdyksen saapumisaika on ihan täysin |
481 # Fölin datassa on jotain tosi kummaa. Ilmeisesti ajovuoron viimeisen pysähdyksen saapumisaika on ihan täysin |
456 # väärin. Arvaan että se on seuraavan lähdön aika, mutta joka tapauksessa se on väärin. |
482 # väärin. Arvaan että se on seuraavan lähdön aika, mutta joka tapauksessa se on väärin. |
457 # Arvataan mikä se todellinen saapumisaika on. Se ei voi mennä kauhean paljon pahemmin vikaan kuin alkuperäinen |
483 # Arvataan mikä se todellinen saapumisaika on. Se ei voi mennä kauhean paljon pahemmin vikaan kuin alkuperäinen |
458 # väärin oleva data. |
484 # väärin oleva data. |
459 for trip in all_trips.values(): |
485 for trip in all_trips.values(): |
460 bus_speed_coefficient = 750 # metriä minuutissa |
486 if len(trip.schedule) >= 2: |
461 last_leg_distance = trip.schedule[-1].traveled_distance - trip.schedule[-2].traveled_distance |
487 bus_speed_coefficient = 750 # metriä minuutissa |
462 trip.schedule[-1].arrival_time = trip.schedule[-2].departure_time + timedelta(minutes = last_leg_distance / bus_speed_coefficient) |
488 last_leg_distance = trip.schedule[-1].traveled_distance - trip.schedule[-2].traveled_distance |
|
489 trip.schedule[-1].arrival_time = trip.schedule[-2].departure_time + timedelta(minutes = last_leg_distance / bus_speed_coefficient) |
|
490 |
|
491 global trips_by_vehicle_info |
|
492 trips_by_vehicle_info = {} |
|
493 for trip in all_trips.values(): |
|
494 trips_by_vehicle_info[(trip.block_id, trip.schedule[0].arrival_time)] = trip |
463 |
495 |
464 if __name__ == '__main__': |
496 if __name__ == '__main__': |
465 from configparser import ConfigParser |
|
466 profile = ConfigParser() |
|
467 profile.read('profiles/föli.ini') |
497 profile.read('profiles/föli.ini') |
468 load_buses('gtfs.zip', profile) |
498 load_buses('gtfs.zip') |