diff --git a/checkers/common.py b/checkers/common.py new file mode 100644 index 0000000..bc8b809 --- /dev/null +++ b/checkers/common.py @@ -0,0 +1,138 @@ +import logging +import math +import functools +from itertools import chain + + +"""A coordinate of a station precision of which we must take into account +is calculated as an average of somewhat 10 elements. +Taking machine epsilon 1e-15, averaging 10 numbers with close magnitudes +ensures relative precision of 1e-14.""" +coord_isclose = functools.partial(math.isclose, rel_tol=1e-14) + + +def coords_eq(lon1, lat1, lon2, lat2): + return coord_isclose(lon1, lon2) and coord_isclose(lat1, lat2) + + +def osm_id_comparator(el): + """This function is used as key for sorting lists of + OSM-originated objects + """ + return (el['osm_type'], el['osm_id']) + + +def compare_stops(stop0, stop1): + """Compares json of two stops in route""" + stop_keys = ('name', 'int_name', 'id', 'osm_id', 'osm_type') + stop0_props = tuple(stop0[k] for k in stop_keys) + stop1_props = tuple(stop1[k] for k in stop_keys) + + if stop0_props != stop1_props: + logging.debug("Different stops properties: %s, %s", + stop0_props, stop1_props) + return False + + if not coords_eq(stop0['lon'], stop0['lat'], + stop1['lon'], stop1['lat']): + logging.debug("Different stops coordinates: %s (%f, %f), %s (%f, %f)", + stop0_props, stop0['lon'], stop0['lat'], + stop1_props, stop1['lon'], stop1['lat']) + return False + + entrances0 = sorted(stop0['entrances'], key=osm_id_comparator) + entrances1 = sorted(stop1['entrances'], key=osm_id_comparator) + if entrances0 != entrances1: + logging.debug("Different stop entrances") + return False + + exits0 = sorted(stop0['exits'], key=osm_id_comparator) + exits1 = sorted(stop1['exits'], key=osm_id_comparator) + if exits0 != exits1: + logging.debug("Different stop exits") + return False + + return True + + +def compare_transfers(transfers0, transfers1): + """Compares two arrays of transfers of the form + [(stop1_uid, stop2_uid, time), ...] + """ + if len(transfers0) != len(transfers1): + logging.debug("Different len(transfers): %d != %d", + len(transfers0), len(transfers1)) + return False + + transfers0 = [tuple([t[0], t[1], t[2]]) + if t[0] < t[1] else + tuple([t[1], t[0], t[2]]) + for t in transfers0] + transfers1 = [tuple([t[0], t[1], t[2]]) + if t[0] < t[1] else + tuple([t[1], t[0], t[2]]) + for t in transfers1] + + transfers0.sort() + transfers1.sort() + + diff_cnt = 0 + for tr0, tr1 in zip(transfers0, transfers1): + if tr0 != tr1: + if diff_cnt == 0: + logging.debug("First pair of different transfers: %s, %s", + tr0, tr1) + diff_cnt += 1 + if diff_cnt: + logging.debug("Different transfers number = %d", diff_cnt) + return False + + return True + + +def compare_networks(network0, network1): + if network0['agency_id'] != network1['agency_id']: + logging.debug("Different agency_id at route '%s'", + network0['network']) + return False + + route_ids0 = sorted(x['route_id'] for x in network0['routes']) + route_ids1 = sorted(x['route_id'] for x in network1['routes']) + + if route_ids0 != route_ids1: + logging.debug("Different route_ids: %s != %s", + route_ids0, route_ids1) + return False + + routes0 = sorted(network0['routes'], key=lambda x: x['route_id']) + routes1 = sorted(network1['routes'], key=lambda x: x['route_id']) + + # Keys to compare routes. 'name' key is omitted since RouteMaster + # can get its name from one of its Routes unpredictably. + route_keys = ('type', 'ref', 'colour', 'route_id') + + for route0, route1 in zip(routes0, routes1): + route0_props = tuple(route0[k] for k in route_keys) + route1_props = tuple(route1[k] for k in route_keys) + if route0_props != route1_props: + logging.debug("Route props of '%s' are different: %s, %s", + route0['route_id'], route0_props, route1_props) + return False + + itineraries0 = sorted(route0['itineraries'], + key=lambda x: tuple(chain(*x['stops']))) + itineraries1 = sorted(route1['itineraries'], + key=lambda x: tuple(chain(*x['stops']))) + + for itin0, itin1 in zip(itineraries0, itineraries1): + if itin0['interval'] != itin1['interval']: + logging.debug("Different interval: %d != %d at route %s '%s'", + itin0['interval'], itin1['interval'], + route0['route_id'], route0['name']) + return False + if itin0['stops'] != itin1['stops']: + logging.debug("Different stops at route %s '%s'", + route0['route_id'], route0['name']) + return False + + return True diff --git a/checkers/compare_city_caches.py b/checkers/compare_city_caches.py new file mode 100644 index 0000000..c6c81bc --- /dev/null +++ b/checkers/compare_city_caches.py @@ -0,0 +1,65 @@ +"""This utility allows one to check equivalency of generated city caches + (defined by --cache command line parameter) of process_subways.py. + + Due to unordered nature of sets/dicts, two runs of process_subways.py + even on the same input generate equivalent jsons, + which cannot be compared with 'diff' command. The compare_jsons() function + compares two city_cache.json taking into account possible shuffling of + dict items and items of some lists, as well as system-specific subtleties. + This utility is useful to ensure that code improvements which must not + affect the process_subways.py output really doesn't change it. +""" + +import sys +import json +import logging +from common import compare_stops, compare_transfers, compare_networks + + +def compare_jsons(cache0, cache1): + """Compares two city caches""" + + city_names0 = sorted(cache0.keys()) + city_names1 = sorted(cache1.keys()) + if city_names0 != city_names1: + logging.debug("Different list of city names!") + return False + + for name in city_names0: + city0 = cache0[name] + city1 = cache1[name] + if not compare_networks(city0['network'], city1['network']): + return False + + stop_ids0 = sorted(city0['stops'].keys()) + stop_ids1 = sorted(city1['stops'].keys()) + if stop_ids0 != stop_ids1: + logging.debug("Different stop_ids") + return False + stops0 = [v for k, v in sorted(city0['stops'].items())] + stops1 = [v for k, v in sorted(city1['stops'].items())] + for stop0, stop1 in zip(stops0, stops1): + if not compare_stops(stop0, stop1): + return False + + if not compare_transfers(city0['transfers'], city1['transfers']): + return False + + return True + + +if __name__ == "__main__": + if len(sys.argv) != 3: + print("Usage: {} ".format(sys.argv[0])) + sys.exit() + + logging.basicConfig(level=logging.DEBUG) + + path0, path1 = sys.argv[1:3] + + j0 = json.load(open(path0, encoding='utf-8')) + j1 = json.load(open(path1, encoding='utf-8')) + + equal = compare_jsons(j0, j1) + + print("The city caches are {}equal".format("" if equal else "NOT ")) diff --git a/checkers/compare_json_outputs.py b/checkers/compare_json_outputs.py index e0c38ea..8ded974 100644 --- a/checkers/compare_json_outputs.py +++ b/checkers/compare_json_outputs.py @@ -12,129 +12,36 @@ import sys import json -from itertools import chain +import logging +from common import compare_stops, compare_transfers, compare_networks def compare_jsons(result0, result1): - - def floats_eq(a, b): - return abs(b - a) < 1e-14 - - def coords_eq(lon1, lat1, lon2, lat2): - return floats_eq(lon1, lon2) and floats_eq(lat1, lat2) - - def osm_id_comparator(el): - return (el['osm_type'], el['osm_id']) + """Compares two objects which are results of subway generation""" network_names0 = sorted([x['network'] for x in result0['networks']]) network_names1 = sorted([x['network'] for x in result1['networks']]) - if network_names0 != network_names1: - print("Different list of network names!") + logging.debug("Different list of network names!") return False - networks0 = sorted(result0['networks'], key=lambda x: x['network']) networks1 = sorted(result1['networks'], key=lambda x: x['network']) - - # Keys to compare routes. 'name' key is omitted since RouteMaster - # can get its name from one of its Routes unpredictably. - route_keys = ('type', 'ref', 'colour', 'route_id') - for network0, network1 in zip(networks0, networks1): - if network0['agency_id'] != network1['agency_id']: - print("Different agency_id:", - network0['network'], network1['network']) + if not compare_networks(network0, network1): return False - route_ids0 = sorted(x['route_id'] for x in network0['routes']) - route_ids1 = sorted(x['route_id'] for x in network1['routes']) - - if route_ids0 != route_ids1: - print("Different route_ids", route_ids0, route_ids1) - return False - - routes0 = sorted(network0['routes'], key=lambda x: x['route_id']) - routes1 = sorted(network1['routes'], key=lambda x: x['route_id']) - - for route0, route1 in zip(routes0, routes1): - route0_props = tuple(route0[k] for k in route_keys) - route1_props = tuple(route1[k] for k in route_keys) - if route0_props != route1_props: - print("Route props of ", route0['route_id'], route1['route_id'], - "are different:", route0_props, route1_props) - return False - - itineraries0 = sorted(route0['itineraries'], - key=lambda x: tuple(chain(*x['stops']))) - itineraries1 = sorted(route1['itineraries'], - key=lambda x: tuple(chain(*x['stops']))) - - for itin0, itin1 in zip(itineraries0, itineraries1): - if itin0['interval'] != itin1['interval']: - print("Different interval:", - f"{itin0['interval']} != {itin1['interval']}" - f" at route {route0['name']} {route0['route_id']}") - return False - if itin0['stops'] != itin1['stops']: - print(f"Different stops at route", - f"{route0['name']} {route0['route_id']}") - return False - stop_ids0 = sorted(x['id'] for x in result0['stops']) stop_ids1 = sorted(x['id'] for x in result1['stops']) if stop_ids0 != stop_ids1: - print("Different stop_ids") + logging.debug("Different stop_ids") return False - stops0 = sorted(result0['stops'], key=lambda x: x['id']) stops1 = sorted(result1['stops'], key=lambda x: x['id']) - for stop0, stop1 in zip(stops0, stops1): - stop0_props = tuple(stop0[k] for k in ('name', 'osm_id', 'osm_type')) - stop1_props = tuple(stop1[k] for k in ('name', 'osm_id', 'osm_type')) - if stop0_props != stop1_props: - print("Different stops properties:", stop0_props, stop1_props) - return False - if not coords_eq(stop0['lon'], stop0['lat'], - stop1['lon'], stop1['lat']): - print("Different stops coordinates:", - stop0_props, stop0['lon'], stop0['lat'], - stop1_props, stop1['lon'], stop1['lat']) + if not compare_stops(stop0, stop1): return False - entrances0 = sorted(stop0['entrances'], key=osm_id_comparator) - entrances1 = sorted(stop1['entrances'], key=osm_id_comparator) - if entrances0 != entrances1: - print("Different stop entrances") - return False - - exits0 = sorted(stop0['exits'], key=osm_id_comparator) - exits1 = sorted(stop1['exits'], key=osm_id_comparator) - if exits0 != exits1: - print("Different stop exits") - return False - - if len(result0['transfers']) != len(result1['transfers']): - print("Different len(transfers):", - len(result0['transfers']), len(result1['transfers'])) - return False - - transfers0 = [tuple(t) if t[0] < t[1] else tuple([t[1], t[0], t[2]]) - for t in result0['transfers']] - transfers1 = [tuple(t) if t[0] < t[1] else tuple([t[1], t[0], t[2]]) - for t in result1['transfers']] - - transfers0.sort(key=lambda x: tuple(x)) - transfers1.sort(key=lambda x: tuple(x)) - - diff_cnt = 0 - for i, (tr0, tr1) in enumerate(zip(transfers0, transfers1)): - if tr0 != tr1: - if i == 0: - print("First pair of different transfers", tr0, tr1) - diff_cnt += 1 - if diff_cnt: - print("Different transfers number = ", diff_cnt) + if not compare_transfers(result0['transfers'], result1['transfers']): return False return True @@ -145,6 +52,8 @@ if __name__ == "__main__": print("Usage: {} ".format(sys.argv[0])) sys.exit() + logging.basicConfig(level=logging.DEBUG) + path0, path1 = sys.argv[1:3] j0 = json.load(open(path0, encoding='utf-8')) @@ -152,4 +61,4 @@ if __name__ == "__main__": equal = compare_jsons(j0, j1) - print("The results are {}equal".format("" if equal else "NOT ")) \ No newline at end of file + print("The results are {}equal".format("" if equal else "NOT ")) diff --git a/process_subways.py b/process_subways.py index 1e0d8dc..33c9453 100755 --- a/process_subways.py +++ b/process_subways.py @@ -406,5 +406,5 @@ if __name__ == '__main__': json.dump(res, options.log) if options.output: - json.dump(processor.process(good_cities, transfers, options.cache), + json.dump(processor.process(cities, transfers, options.cache), options.output, indent=1, ensure_ascii=False) diff --git a/processors/mapsme.py b/processors/mapsme.py index ddbf4a8..48391b5 100755 --- a/processors/mapsme.py +++ b/processors/mapsme.py @@ -1,24 +1,166 @@ import json -from subway_structure import distance +import os +import logging +from collections import defaultdict +from subway_structure import distance, el_center, Station OSM_TYPES = {'n': (0, 'node'), 'w': (2, 'way'), 'r': (3, 'relation')} ENTRANCE_PENALTY = 60 # seconds -SPEED_TO_ENTRANCE = 5 # km/h -SPEED_ON_TRANSFER = 3.5 -SPEED_ON_LINE = 40 +TRANSFER_PENALTY = 30 # seconds +KMPH_TO_MPS = 1/3.6 # km/h to m/s conversion multiplier +SPEED_TO_ENTRANCE = 5 * KMPH_TO_MPS # m/s +SPEED_ON_TRANSFER = 3.5 * KMPH_TO_MPS # m/s +SPEED_ON_LINE = 40 * KMPH_TO_MPS # m/s DEFAULT_INTERVAL = 2.5 # minutes +DISPLACEMENT_TOLERANCE = 300 # meters -def process(cities, transfers, cache_name): - def uid(elid, typ=None): - t = elid[0] - osm_id = int(elid[1:]) - if not typ: - osm_id = osm_id << 2 + OSM_TYPES[t][0] - elif typ != t: - raise Exception('Got {}, expected {}'.format(elid, typ)) - return osm_id << 1 +def uid(elid, typ=None): + t = elid[0] + osm_id = int(elid[1:]) + if not typ: + osm_id = (osm_id << 2) + OSM_TYPES[t][0] + elif typ != t: + raise Exception('Got {}, expected {}'.format(elid, typ)) + return osm_id << 1 + + +class DummyCache: + """This class may be used when you need to omit all cache processing""" + + def __init__(self, cache_path, cities): + pass + + def __getattr__(self, name): + """This results in that a call to any method effectively does nothing + and does not generate exceptions.""" + def method(*args, **kwargs): + return None + return method + + +def if_object_is_used(method): + """Decorator to skip method execution under certain condition. + Relies on "is_used" object property.""" + def inner(self, *args, **kwargs): + if not self.is_used: + return + return method(self, *args, **kwargs) + return inner + + +class MapsmeCache: + def __init__(self, cache_path, cities): + if not cache_path: + # cache is not used, all actions with cache must be silently skipped + self.is_used = False + return + self.cache_path = cache_path + self.is_used = True + self.cache = {} + if os.path.exists(cache_path): + try: + with open(cache_path, 'r', encoding='utf-8') as f: + self.cache = json.load(f) + except json.decoder.JSONDecodeError: + logging.warning("City cache '%s' is not a valid json file. " + "Building cache from scratch.", cache_path) + self.recovered_city_names = set() + # One stoparea may participate in routes of different cities + self.stop_cities = defaultdict(set) # stoparea id -> city names + self.city_dict = {c.name: c for c in cities} + self.good_city_names = {c.name for c in cities if c.is_good()} + + def _is_cached_city_usable(self, city): + """Check if cached stations still exist in osm data and + not moved far away. + """ + city_cache_data = self.cache[city.name] + for stoparea_id, cached_stoparea in city_cache_data['stops'].items(): + station_id = cached_stoparea['osm_type'][0] + str(cached_stoparea['osm_id']) + city_station = city.elements.get(station_id) + if (not city_station or + not Station.is_station(city_station, city.modes)): + return False + station_coords = el_center(city_station) + cached_station_coords = tuple(cached_stoparea[coord] for coord in ('lon', 'lat')) + displacement = distance(station_coords, cached_station_coords) + if displacement > DISPLACEMENT_TOLERANCE: + return False + + return True + + @if_object_is_used + def provide_stops_and_networks(self, stops, networks): + """Put stops and networks for bad cities into containers + passed as arguments.""" + for city in self.city_dict.values(): + if not city.is_good() and city.name in self.cache: + city_cached_data = self.cache[city.name] + if self._is_cached_city_usable(city): + stops.update(city_cached_data['stops']) + networks.append(city_cached_data['network']) + logging.info("Taking %s from cache", city.name) + self.recovered_city_names.add(city.name) + + @if_object_is_used + def provide_transfers(self, transfers): + """Add transfers from usable cached cities to 'transfers' dict + passed as argument.""" + for city_name in self.recovered_city_names: + city_cached_transfers = self.cache[city_name]['transfers'] + for stop1_uid, stop2_uid, transfer_time in city_cached_transfers: + if (stop1_uid, stop2_uid) not in transfers: + transfers[(stop1_uid, stop2_uid)] = transfer_time + + @if_object_is_used + def initialize_good_city(self, city_name, network): + """Create/replace one cache element with new data container. + This should be done for each good city.""" + self.cache[city_name] = { + 'network': network, + 'stops': {}, # stoparea el_id -> jsonified stop data + 'transfers': [] # list of tuples (stoparea1_uid, stoparea2_uid, time); uid1 < uid2 + } + + @if_object_is_used + def link_stop_with_city(self, stoparea_id, city_name): + """Remember that some stop_area is used in a city.""" + stoparea_uid = uid(stoparea_id) + self.stop_cities[stoparea_uid].add(city_name) + + @if_object_is_used + def add_stop(self, stoparea_id, st): + """Add stoparea to the cache of each city the stoparea is in.""" + stoparea_uid = uid(stoparea_id) + for city_name in self.stop_cities[stoparea_uid]: + self.cache[city_name]['stops'][stoparea_id] = st + + @if_object_is_used + def add_transfer(self, stoparea1_uid, stoparea2_uid, transfer_time): + """If a transfer is inside a good city, add it to the city's cache.""" + for city_name in (self.good_city_names & + self.stop_cities[stoparea1_uid] & + self.stop_cities[stoparea2_uid]): + self.cache[city_name]['transfers'].append( + (stoparea1_uid, stoparea2_uid, transfer_time) + ) + + @if_object_is_used + def save(self): + try: + with open(self.cache_path, 'w', encoding='utf-8') as f: + json.dump(self.cache, f, ensure_ascii=False) + except Exception as e: + logging.warning("Failed to save cache: %s", str(e)) + + +def process(cities, transfers, cache_path): + """cities - list of City instances; + transfers - list of sets of StopArea.id; + cache_path - path to json-file with good cities cache or None. + """ def format_colour(c): return c[1:] if c else None @@ -42,23 +184,19 @@ def process(cities, transfers, cache_name): exits.append(n) return exits - cache = {} - if cache_name: - with open(cache_name, 'r', encoding='utf-8') as f: - cache = json.load(f) - stops = {} # el_id -> station data + cache = MapsmeCache(cache_path, cities) + + stop_areas = {} # stoparea el_id -> StopArea instance + stops = {} # stoparea el_id -> stop jsonified data networks = [] - - good_cities = set([c.name for c in cities]) - for city_name, data in cache.items(): - if city_name in good_cities: - continue - # TODO: get a network, stops and transfers from cache - + good_cities = [c for c in cities if c.is_good()] platform_nodes = {} - for city in cities: + cache.provide_stops_and_networks(stops, networks) + + for city in good_cities: network = {'network': city.name, 'routes': [], 'agency_id': city.id} + cache.initialize_good_city(city.name, network) for route in city: routes = { 'type': route.mode, @@ -74,8 +212,9 @@ def process(cities, transfers, cache_name): for i, variant in enumerate(route): itin = [] for stop in variant: - stops[stop.stoparea.id] = stop.stoparea - itin.append([uid(stop.stoparea.id), round(stop.distance*3.6/SPEED_ON_LINE)]) + stop_areas[stop.stoparea.id] = stop.stoparea + cache.link_stop_with_city(stop.stoparea.id, city.name) + itin.append([uid(stop.stoparea.id), round(stop.distance/SPEED_ON_LINE)]) # Make exits from platform nodes, if we don't have proper exits if len(stop.stoparea.entrances) + len(stop.stoparea.exits) == 0: for pl in stop.stoparea.platforms: @@ -105,8 +244,7 @@ def process(cities, transfers, cache_name): network['routes'].append(routes) networks.append(network) - m_stops = [] - for stop in stops.values(): + for stop_id, stop in stop_areas.items(): st = { 'name': stop.name, 'int_name': stop.int_name, @@ -127,7 +265,7 @@ def process(cities, transfers, cache_name): 'lon': stop.centers[e][0], 'lat': stop.centers[e][1], 'distance': ENTRANCE_PENALTY + round(distance( - stop.centers[e], stop.center)*3.6/SPEED_TO_ENTRANCE) + stop.centers[e], stop.center)/SPEED_TO_ENTRANCE) }) if len(stop.entrances) + len(stop.exits) == 0: if stop.platforms: @@ -140,7 +278,7 @@ def process(cities, transfers, cache_name): 'lon': n['lon'], 'lat': n['lat'], 'distance': ENTRANCE_PENALTY + round(distance( - (n['lon'], n['lat']), stop.center)*3.6/SPEED_TO_ENTRANCE) + (n['lon'], n['lat']), stop.center)/SPEED_TO_ENTRANCE) }) else: for k in ('entrances', 'exits'): @@ -152,28 +290,37 @@ def process(cities, transfers, cache_name): 'distance': 60 }) - m_stops.append(st) + stops[stop_id] = st + cache.add_stop(stop_id, st) - c_transfers = [] + pairwise_transfers = {} # (stoparea1_uid, stoparea2_uid) -> time; uid1 < uid2 for t_set in transfers: t = list(t_set) for t_first in range(len(t) - 1): for t_second in range(t_first + 1, len(t)): - if t[t_first].id in stops and t[t_second].id in stops: - c_transfers.append([ - uid(t[t_first].id), - uid(t[t_second].id), - 30 + round(distance(t[t_first].center, - t[t_second].center)*3.6/SPEED_ON_TRANSFER) - ]) + stoparea1 = t[t_first] + stoparea2 = t[t_second] + if stoparea1.id in stops and stoparea2.id in stops: + uid1 = uid(stoparea1.id) + uid2 = uid(stoparea2.id) + uid1, uid2 = sorted([uid1, uid2]) + transfer_time = (TRANSFER_PENALTY + + round(distance(stoparea1.center, + stoparea2.center) + / SPEED_ON_TRANSFER)) + pairwise_transfers[(uid1, uid2)] = transfer_time + cache.add_transfer(uid1, uid2, transfer_time) - if cache_name: - with open(cache_name, 'w', encoding='utf-8') as f: - json.dump(cache, f) + cache.provide_transfers(pairwise_transfers) + cache.save() + + pairwise_transfers = [(stop1_uid, stop2_uid, transfer_time) + for (stop1_uid, stop2_uid), transfer_time + in pairwise_transfers.items()] result = { - 'stops': m_stops, - 'transfers': c_transfers, + 'stops': list(stops.values()), + 'transfers': pairwise_transfers, 'networks': networks } return result diff --git a/scripts/process_subways.sh b/scripts/process_subways.sh index 43ca02e..63530a4 100755 --- a/scripts/process_subways.sh +++ b/scripts/process_subways.sh @@ -71,7 +71,9 @@ QNODES="railway=station station=subway =light_rail =monorail railway=subway_entr # Running the validation VALIDATION="$TMPDIR/validation.json" -"$PYTHON" "$SUBWAYS_PATH/process_subways.py" -q -x "$FILTERED_DATA" -l "$VALIDATION" ${MAPSME+-o "$MAPSME"} ${CITY+-c "$CITY"} ${DUMP+-d "$DUMP"} ${JSON+-j "$JSON"} +"$PYTHON" "$SUBWAYS_PATH/process_subways.py" -q -x "$FILTERED_DATA" -l "$VALIDATION" ${MAPSME+-o "$MAPSME"}\ + ${CITY+-c "$CITY"} ${DUMP+-d "$DUMP"} ${JSON+-j "$JSON"}\ + ${ELEMENTS_CACHE+-i "$ELEMENTS_CACHE"} ${CITY_CACHE+--cache "$CITY_CACHE"} rm "$FILTERED_DATA" # Preparing HTML files diff --git a/subway_structure.py b/subway_structure.py index 4abc310..c54c2db 100644 --- a/subway_structure.py +++ b/subway_structure.py @@ -23,7 +23,6 @@ RAILWAY_TYPES = set(('rail', 'light_rail', 'subway', 'narrow_gauge', CONSTRUCTION_KEYS = ('construction', 'proposed', 'construction:railway', 'proposed:railway') NOWHERE_STOP = (0, 0) # too far away from any metro system -transfers = [] used_entrances = set() @@ -1213,7 +1212,6 @@ class City: def find_transfers(elements, cities): - global transfers transfers = [] stop_area_groups = [] for el in elements: