diff -r 3a495bc4b7b5 -r e6bdb9c54096 buses.py --- a/buses.py Tue Jun 20 09:39:42 2017 +0300 +++ b/buses.py Tue Jun 20 10:27:52 2017 +0300 @@ -5,82 +5,79 @@ from copy import copy from misc import * from geometry import * -Suunta = enum.Enum('Suunta', [('Taaksepäin', 0), ('Eteenpäin', 1)]) -def muunna_ajovuoro_tunniste(tunniste): - return tunniste +def transform_trip_reference(reference): + return reference -class Ajovuoro: - def __init__(self, tunniste, linja, palvelu, kyltti, suunta, length): - self.tunniste, self.linja, self.palvelu, self.kyltti, self.suunta = tunniste, linja, \ - palvelu, kyltti, suunta +class BusTrip: + def __init__(self, reference, route, service, length): + self.reference, self.route, self.service = reference, route, service self.length = length - self.reitti = [] - self.nimi = muunna_ajovuoro_tunniste(tunniste) + self.schedule = [] + self.name = transform_trip_reference(reference) def __repr__(self): - return 'ajot[%r]' % self.nimi - def pysäkkiReitillä(self, pysäkki): - for pysähdys in self.reitti: - if pysähdys.pysäkki is pysäkki: - return pysähdys + return 'all_trips[%r]' % self.name + def contains_stop(self, stop): + for halt in self.schedule: + if halt.stop is stop: + return halt else: return None - def ajetaan_päivänä(self, päivä): + def is_served_at(self, day): try: - return self.palvelu in palvelut_per_päivä[päivä] + return self.service in services_for_day[day] except KeyError: return False - def suppea_reitti(self, pysäkistä = None): - if pysäkistä and pysäkistä in self.reitti: - reitti = copy(self.reitti) - reitti = reitti[reitti.index(pysäkistä):] + def concise_schedule(self, starting_stop = None): + if starting_stop and starting_stop in self.schedule: + schedule = copy(self.schedule) + schedule = schedule[schedule.index(starting_stop):] else: - reitti = self.reitti - käytetyt_alueet = set() - tulos = [] - for pysähdys in reitti: - pysäkki = pysähdys.pysäkki - if pysäkki.alue and pysäkki.alue not in käytetyt_alueet: - käytetyt_alueet.add(pysäkki.alue) - tulos.append(pysäkki.alue) - return tulos + schedule = self.schedule + used_areas = set() + result = [] + for halt in schedule: + stop = halt.stop + if stop.region and stop.region not in used_areas: + used_areas.add(stop.region) + result.append(stop.region) + return result -class Linja: - def __init__(self, tietue): - self.tunniste = tietue['route_id'] - self.viite = tietue['route_short_name'] - self.selite = tietue['route_long_name'] +class BusRoute: + def __init__(self, entry): + self.id = entry['route_id'] + self.reference = entry['route_short_name'] def __repr__(self): - return 'linjat[%r]' % self.viite + return 'routes[%r]' % self.reference -class Palvelu: - def __init__(self, tunniste): - self.tunniste = tunniste - self.päivät = set() +class BusService: + def __init__(self, reference): + self.reference = reference + self.dates = set() def __repr__(self): - return 'palvelut[%r]' % self.tunniste + return 'services[%r]' % self.reference -class Pysäkki: - def __init__(self, tunniste, nimi, sijainti): - self.tunniste, self.nimi, self.sijainti = tunniste, nimi, sijainti +class BusStop: + def __init__(self, reference, name, location): + self.reference, self.name, self.location = reference, name, location self.cluster = None self.pairs = set() # samannimiset lähellä olevat pysäkit def __repr__(self): - return 'pysäkit[%r]' % self.tunniste + return 'bus_stops[%r]' % self.reference def schedule(self, max_amount = 50): ''' Hakee tämän pysäkin seuraavat `määrä` lähtöä. Päätepysäkille saapuvia busseja ei - lasketa. Palauttaa pysähdykset listana jossa alkiot ovat muotoa (aika, pysähdys), + lasketa. Palauttaa pysähdykset listana jossa alkiot ovat muotoa (aika, halt), jossa: - `aika` on saapumishetki muotoa datetime ja - - `pysähdys` on vastaava Pysähdys olio. + - `halt` on vastaava BusHalt olio. Mikäli pysäkille ei ole määrätty riittävästi pysähdyksiä kalenterissa, tuloslista jää alimittaiseksi, mahdollisesti jopa tyhjäksi. ''' result = [] # -1 päivää yövuoroja varten - date = tänään() - timedelta(days = 1) + date = today() - timedelta(days = 1) # Niin kauan kuin aikatauluja ei ole vielä tarpeeksi, while len(result) < max_amount: try: @@ -98,22 +95,22 @@ Hakee pysäkin aikataulut tiettynä päivänä. ''' # Jos päädyttiin aikataulukalenterin ulkopuolelle, niin tuotetaan virhe. Jos vain - # palautettaisiin tyhjä tulos, niin algoritmi jatkaisi etsintää loputtomiin. + # palautettaisiin tyhjä result, niin algoritmi jatkaisi etsintää loputtomiin. if date > viimeinen_käyttöpäivä: raise ValueError('tried to retrieve schedule for date %s which is outside schedule data' % date) result = [] # Jokaiselle ajovuorolle, - for trip in ajot.values(): + for trip in all_trips.values(): # jos tämä ajovuoro ajetaan tänä päivänä - if trip.ajetaan_päivänä(date): - # ja jos tämä ajo pysähtyy tällä pysäkillä, ei kuitenkaan saapuen + if trip.is_served_at(date): + # ja jos tämä trip pysähtyy tällä pysäkillä, ei kuitenkaan saapuen # päätepysäkille, - stop = trip.pysäkkiReitillä(self) - if stop and not stop.isArrival: # stop is not trip.reitti[-1]: - # ja jos tämä pysähdys on tulevaisuudessa, - stop_time = datetime.combine(date, time()) + stop.saapumisaika - if stop_time >= nyt(): - # lisää pysähdys listaan. + stop = trip.contains_stop(self) + if stop and not stop.isArrival: # stop is not trip.schedule[-1]: + # ja jos tämä halt on tulevaisuudessa, + stop_time = datetime.combine(date, time()) + stop.arrival_time + if stop_time >= now(): + # lisää halt listaan. result.append({ 'time': stop_time, 'trip': trip, @@ -123,107 +120,102 @@ result.sort(key = lambda schedule_entry: schedule_entry['time']) return result -class Pysähdys: - def __init__(self, saapumisaika, lähtöaika, pysäkki, ajo, ajettu_matka): - self.saapumisaika, self.lähtöaika, self.pysäkki, self.ajo = saapumisaika, lähtöaika, \ - pysäkki, ajo - self.ajettu_matka = ajettu_matka - self._isArrival = None +class BusHalt: + def __init__(self, arrival_time, departure_time, stop, trip, traveled_distance): + self.arrival_time, self.departure_time, self.stop, self.trip = arrival_time, departure_time, \ + stop, trip + self.traveled_distance = traveled_distance @property def isArrival(self): - if self._isArrival is None: - iterator = iter(self.ajo.reitti) + if not hasattr(self, 'cachedIsArrival'): + iterator = iter(self.trip.schedule) stop = next(iterator) while stop is not self: stop = next(iterator) for stop in iterator: - if stop.pysäkki.alue != self.pysäkki.alue: - self._isArrival = False + if stop.stop.region != self.stop.region: + self.cachedIsArrival = False break else: - self._isArrival = True - return self._isArrival + self.cachedIsArrival = True + return self.cachedIsArrival def __repr__(self): - return 'Pysähdys(%r, %r, %r, %r)' % (self.saapumisaika, self.lähtöaika, self.pysäkki, self.ajo) + return 'BusHalt(%r, %r, %r, %r)' % (self.arrival_time, self.departure_time, self.stop, self.trip) -linjat = {} -linjat_per_tunniste = {} -ajot = {} -ajot_per_numero = {} -palvelut = {} -pysäkit = {} +routes = {} +routes_per_id = {} +all_trips = {} +services = {} +bus_stops = {} all_clusters = set() -print('Ladataan linjat... ', file = stderr, end = '', flush = True) -with open('gtfs/routes.txt') as tiedosto: - for rivi in lue_csv(tiedosto): - linja = Linja(rivi) - linja.tunniste = linja.tunniste - linjat[linja.viite] = linja - linjat_per_tunniste[linja.tunniste] = linja -print('%d linjaa' % len(linjat), file = stderr) +print('Ladataan routes... ', file = stderr, end = '', flush = True) +with open('gtfs/routes.txt') as file: + for row in read_csv(file): + route = BusRoute(row) + routes[route.reference] = route + routes_per_id[route.id] = route +print('%d linjaa' % len(routes), file = stderr) -print('Ladataan ajot... ', file = stderr, end = '', flush = True) +print('Ladataan ajovuorot... ', file = stderr, end = '', flush = True) shape_distances = {} with open('gtfs/shapes.txt') as file: - for row in lue_csv(file): + for row in read_csv(file): shape_distances[row['shape_id']] = max(shape_distances.get(row['shape_id'], 0), float(row['shape_dist_traveled'])) -with open('gtfs/trips.txt') as tiedosto: - for rivi in lue_csv(tiedosto, muunnokset = {'direction_id': lambda k: Suunta(int(k))}): - if rivi['service_id'] not in palvelut: - palvelut[rivi['service_id']] = Palvelu(rivi['service_id']) - linja = linjat_per_tunniste[rivi['route_id']] - ajo = Ajovuoro( - tunniste = rivi['trip_id'], - linja = linja, - palvelu = palvelut[rivi['service_id']], - kyltti = rivi['trip_headsign'], - suunta = rivi['direction_id'], - length = shape_distances[rivi['shape_id']] +with open('gtfs/trips.txt') as file: + for row in read_csv(file): + if row['service_id'] not in services: + services[row['service_id']] = BusService(row['service_id']) + route = routes_per_id[row['route_id']] + trip = BusTrip( + reference = row['trip_id'], + route = route, + service = services[row['service_id']], + length = shape_distances[row['shape_id']] ) - assert ajo.nimi not in ajot - ajot[ajo.nimi] = ajo -print('%d ajoa' % len(ajot), file = stderr) + assert trip.name not in all_trips + all_trips[trip.name] = trip +print('%d ajoa' % len(all_trips), file = stderr) def lue_päiväys(teksti): return date(int(teksti[:4]), int(teksti[4:6]), int(teksti[6:])) -def lue_aika(teksti): +def read_time(teksti): tunti, minuutti, sekunti = map(int, teksti.split(':')) return timedelta(hours = tunti, minutes = minuutti, seconds = sekunti) print('Ladataan päiväykset... ', file = stderr, flush = True) viimeinen_käyttöpäivä = date.today() -palvelut_per_päivä = {} +services_for_day = {} -with open('gtfs/calendar_dates.txt') as tiedosto: - for rivi in lue_csv(tiedosto): - palvelu = palvelut[rivi['service_id']] - päivä = lue_päiväys(rivi['date']) - palvelu.päivät.add(päivä) - if päivä not in palvelut_per_päivä: - palvelut_per_päivä[päivä] = set() - palvelut_per_päivä[päivä].add(palvelu) - viimeinen_käyttöpäivä = max(päivä, viimeinen_käyttöpäivä) +with open('gtfs/calendar_dates.txt') as file: + for row in read_csv(file): + service = services[row['service_id']] + day = lue_päiväys(row['date']) + service.dates.add(day) + if day not in services_for_day: + services_for_day[day] = set() + services_for_day[day].add(service) + viimeinen_käyttöpäivä = max(day, viimeinen_käyttöpäivä) -def palvelut_käytössä(päivä): - for palvelu in palvelut.values(): - if päivä in palvelu.päivät: - yield palvelu +def services_available_at(day): + for service in services.values(): + if day in service.dates: + yield service print('Ladataan pysäkit... ', file = stderr, end = '', flush = True) with open('gtfs/stops.txt') as file: - for rivi in lue_csv(file): - sijainti = Sijainti(float(rivi['stop_lat']), float(rivi['stop_lon'])) - pysäkki = Pysäkki(rivi['stop_id'], rivi['stop_name'], sijainti) - pysäkit[pysäkki.tunniste] = pysäkki + for row in read_csv(file): + location = Sijainti(float(row['stop_lat']), float(row['stop_lon'])) + stop = BusStop(row['stop_id'], row['stop_name'], location) + bus_stops[stop.reference] = stop with open('regions-per-stop.json') as file: - for pysäkkitunniste, alue in json.load(file).items(): - pysäkit[pysäkkitunniste].alue = alue -print('%d pysäkkiä' % len(pysäkit), file = stderr) + for stop_reference, region in json.load(file).items(): + bus_stops[stop_reference].region = region +print('%d pysäkkiä' % len(bus_stops), file = stderr) class BusStopCluster: @@ -244,10 +236,10 @@ if not self._center: if self.stops: from statistics import median - pointtype = type(next(iter(self.stops)).sijainti) + pointtype = type(next(iter(self.stops)).location) self._center = pointtype( - median(stop.sijainti.x for stop in self.stops), - median(stop.sijainti.y for stop in self.stops), + median(stop.location.x for stop in self.stops), + median(stop.location.y for stop in self.stops), ) else: raise ValueError('an empty cluster has no center point') @@ -267,20 +259,20 @@ from collections import defaultdict bus_stops_by_name = defaultdict(set) -for bus_stop in pysäkit.values(): - bus_stops_by_name[bus_stop.nimi].add(bus_stop) +for bus_stop in bus_stops.values(): + bus_stops_by_name[bus_stop.name].add(bus_stop) bus_stops_by_name = dict(bus_stops_by_name) -# ryhmittele pysäkit nimen mukaan +# ryhmittele bus_stops nimen mukaan all_clusters = [] def cluster_bus_stops(): - sorted_bus_stops = sorted(pysäkit.values(), key = lambda bus_stop: bus_stop.nimi) + sorted_bus_stops = sorted(bus_stops.values(), key = lambda bus_stop: bus_stop.name) for bus_stop in sorted_bus_stops: if not bus_stop.cluster: stops_to_cluster = {bus_stop} # etsi pysäkin samannimiset vastaparit - for pair_candidate in bus_stops_by_name[bus_stop.nimi]: - distance = pair_candidate.sijainti.etäisyys(bus_stop.sijainti) + for pair_candidate in bus_stops_by_name[bus_stop.name]: + distance = pair_candidate.location.etäisyys(bus_stop.location) if pair_candidate is not bus_stop and distance <= 0.3: stops_to_cluster.add(pair_candidate) for stop_to_cluster in stops_to_cluster: @@ -294,16 +286,16 @@ if not stop_to_cluster.cluster: cluster.add_stop(stop_to_cluster) # Merkitse muistiin pysäkkien vastaparit käyttäen hyväksi tämänhetkistä ryhmittelytietoa - for bus_stop in pysäkit.values(): + for bus_stop in bus_stops.values(): if bus_stop.cluster: bus_stop.pairs = bus_stop.cluster.stops - {bus_stop} - # Ryhmitä ne pysäkit, joilla ei ollut omaa vastaparia, muiden pysäkkien kanssa + # Ryhmitä ne bus_stops, joilla ei ollut omaa vastaparia, muiden pysäkkien kanssa for bus_stop in sorted_bus_stops: if len(bus_stop.cluster.stops) == 1: possibilities = set() for cluster in all_clusters: if cluster is not bus_stop.cluster: - distance = cluster.center.etäisyys(bus_stop.sijainti) + distance = cluster.center.etäisyys(bus_stop.location) if distance <= 0.3: possibilities.add((distance, cluster)) if possibilities: @@ -323,18 +315,18 @@ from pprint import pprint clusters_per_name = defaultdict(set) for cluster in all_clusters: - name_representing_stop = min((len(pysäkki.tunniste), pysäkki.tunniste, pysäkki) for pysäkki in cluster.stops)[2] - clusters_per_name[name_representing_stop.nimi].add(cluster) + name_representing_stop = min((len(stop.reference), stop.reference, stop) for stop in cluster.stops)[2] + clusters_per_name[name_representing_stop.name].add(cluster) for name, clusters in clusters_per_name.items(): if len(clusters) == 1: # Ryhmä on ainoa jolla on varaus tälle nimelle. Sen kuin vaan. next(iter(clusters)).name = name else: # Olisiko kaikki klusterit eri alueilla? - common_regions = shared_elements_in_n_sets({stop.alue for stop in cluster.stops} for cluster in clusters) + common_regions = shared_elements_in_n_sets({stop.region for stop in cluster.stops} for cluster in clusters) # Esitys: ryhmä -> ne alueet jotka ovat tälle ryhmälle ainutlaatuisia proposal = { - cluster: {stop.alue for stop in cluster.stops} - common_regions - {None} + cluster: {stop.region for stop in cluster.stops} - common_regions - {None} for cluster in clusters } # Jos enintään yksi klusteri tässä esityksessä on kokonaan ilman omaa aluetta, jolla se voisi eritellä, @@ -349,7 +341,7 @@ else: # Typerä reunatapaus. Indeksoidaan numeroin... for n, (_, cluster) in enumerate(sorted( - min((stop.tunniste.lower(), cluster) for stop in cluster.stops) + min((stop.reference.lower(), cluster) for stop in cluster.stops) for cluster in clusters ), 1): individual_cluster_name = name + '-' + str(n) @@ -367,16 +359,16 @@ print('Ladataan aikataulut... ', end = '', flush = True, file = stderr) with open('gtfs/stop_times.txt') as file: rivimäärä = sum(line.count('\n') for line in file) - laskettu = 0 + progress = 0 file.seek(0) - for rivi in lue_csv(file): - ajo = ajot[muunna_ajovuoro_tunniste(rivi['trip_id'])] - saapumisaika = lue_aika(rivi['arrival_time']) - lähtöaika = lue_aika(rivi['departure_time']) - pysäkki = pysäkit[rivi['stop_id']] - ajettu_matka = float(rivi['shape_dist_traveled']) - ajo.reitti.append(Pysähdys(saapumisaika, lähtöaika, pysäkki, ajo, ajettu_matka)) - laskettu += 1 - if laskettu % 1000 == 0: - print('\rLadataan aikataulut... %.1f%%' % (laskettu * 100 / rivimäärä), end = ' ', file = stderr) + for row in read_csv(file): + trip = all_trips[transform_trip_reference(row['trip_id'])] + arrival_time = read_time(row['arrival_time']) + departure_time = read_time(row['departure_time']) + stop = bus_stops[row['stop_id']] + traveled_distance = float(row['shape_dist_traveled']) + trip.schedule.append(BusHalt(arrival_time, departure_time, stop, trip, traveled_distance)) + progress += 1 + if progress % 1000 == 0: + print('\rLadataan aikataulut... %.1f%%' % (progress * 100 / rivimäärä), end = ' ', file = stderr) print('\rLadataan aikataulut... ladattu', file = stderr)