From 6596d9789cb280d9cc7ff1ac39cecf4d0317c4d1 Mon Sep 17 00:00:00 2001 From: Alexey Zakharenkov Date: Sun, 27 Jun 2021 00:50:37 +0300 Subject: [PATCH] PEPify-8 with black -l 79 -S --- make_all_metro_poly.py | 14 +- mapsme_json_to_cities.py | 6 +- process_subways.py | 206 +++++--- processors/mapsme.py | 183 ++++--- scripts/process_subways.sh | 2 +- stop_areas/make_stop_areas.py | 85 +++- stop_areas/make_tram_areas.py | 68 ++- stop_areas/serve.py | 5 +- subway_io.py | 180 ++++--- subway_structure.py | 919 ++++++++++++++++++++++++---------- validation_to_html.py | 136 +++-- 11 files changed, 1273 insertions(+), 531 deletions(-) diff --git a/make_all_metro_poly.py b/make_all_metro_poly.py index 4276b2b..610892d 100644 --- a/make_all_metro_poly.py +++ b/make_all_metro_poly.py @@ -9,12 +9,14 @@ def make_disjoint_metro_polygons(): polygons = [] for c in cities: - polygon = shapely.geometry.Polygon([ - (c.bbox[1], c.bbox[0]), - (c.bbox[1], c.bbox[2]), - (c.bbox[3], c.bbox[2]), - (c.bbox[3], c.bbox[0]), - ]) + polygon = shapely.geometry.Polygon( + [ + (c.bbox[1], c.bbox[0]), + (c.bbox[1], c.bbox[2]), + (c.bbox[3], c.bbox[2]), + (c.bbox[3], c.bbox[0]), + ] + ) polygons.append(polygon) union = shapely.ops.unary_union(polygons) diff --git a/mapsme_json_to_cities.py b/mapsme_json_to_cities.py index 2449f0b..4b8fea8 100644 --- a/mapsme_json_to_cities.py +++ b/mapsme_json_to_cities.py @@ -18,7 +18,7 @@ if __name__ == '__main__': arg_parser.add_argument( 'subway_json_file', type=argparse.FileType('r'), - help="Validator output defined by -o option of process_subways.py script" + help="Validator output defined by -o option of process_subways.py script", ) arg_parser.add_argument( @@ -33,7 +33,9 @@ if __name__ == '__main__': subway_json_file = args.subway_json_file subway_json = json.load(subway_json_file) - good_cities = set(n.get('network', n.get('title')) for n in subway_json['networks']) + good_cities = set( + n.get('network', n.get('title')) for n in subway_json['networks'] + ) cities = download_cities() lines = [] diff --git a/process_subways.py b/process_subways.py index 806b150..1a25207 100755 --- a/process_subways.py +++ b/process_subways.py @@ -39,13 +39,17 @@ def overpass_request(overground, overpass_api, bboxes): if not overground: query += 'node[railway=subway_entrance]{};'.format(bbox_part) query += 'rel[public_transport=stop_area]{};'.format(bbox_part) - query += 'rel(br)[type=public_transport][public_transport=stop_area_group];' + query += ( + 'rel(br)[type=public_transport][public_transport=stop_area_group];' + ) query += ');(._;>>;);out body center qt;' logging.debug('Query: %s', query) url = '{}?data={}'.format(overpass_api, urllib.parse.quote(query)) response = urllib.request.urlopen(url, timeout=1000) if response.getcode() != 200: - raise Exception('Failed to query Overpass API: HTTP {}'.format(response.getcode())) + raise Exception( + 'Failed to query Overpass API: HTTP {}'.format(response.getcode()) + ) return json.load(response)['elements'] @@ -56,7 +60,11 @@ def multi_overpass(overground, overpass_api, bboxes): for i in range(0, len(bboxes) + SLICE_SIZE - 1, SLICE_SIZE): if i > 0: time.sleep(INTERREQUEST_WAIT) - result.extend(overpass_request(overground, overpass_api, bboxes[i:i+SLICE_SIZE])) + result.extend( + overpass_request( + overground, overpass_api, bboxes[i : i + SLICE_SIZE] + ) + ) return result @@ -66,14 +74,14 @@ def slugify(name): def calculate_centers(elements): """Adds 'center' key to each way/relation in elements, - except for empty ways or relations. - Relies on nodes-ways-relations order in the elements list. + except for empty ways or relations. + Relies on nodes-ways-relations order in the elements list. """ - nodes = {} # id(int) => (lat, lon) - ways = {} # id(int) => (lat, lon) + nodes = {} # id(int) => (lat, lon) + ways = {} # id(int) => (lat, lon) relations = {} # id(int) => (lat, lon) empty_relations = set() # ids(int) of relations without members - # or containing only empty relations + # or containing only empty relations def calculate_way_center(el): # If element has been queried via overpass-api with 'out center;' @@ -108,9 +116,13 @@ def calculate_centers(elements): else: # Center of child relation is not known yet return False - member_container = (nodes if m['type'] == 'node' else - ways if m['type'] == 'way' else - relations) + member_container = ( + nodes + if m['type'] == 'node' + else ways + if m['type'] == 'way' + else relations + ) if m['ref'] in member_container: center[0] += member_container[m['ref']][0] center[1] += member_container[m['ref']][1] @@ -145,54 +157,104 @@ def calculate_centers(elements): relations_without_center = new_relations_without_center if relations_without_center: - logging.error("Cannot calculate center for the relations (%d in total): %s%s", - len(relations_without_center), - ', '.join(str(rel['id']) for rel in relations_without_center[:20]), - ", ..." if len(relations_without_center) > 20 else "") + logging.error( + "Cannot calculate center for the relations (%d in total): %s%s", + len(relations_without_center), + ', '.join(str(rel['id']) for rel in relations_without_center[:20]), + ", ..." if len(relations_without_center) > 20 else "", + ) if empty_relations: - logging.warning("Empty relations (%d in total): %s%s", - len(empty_relations), - ', '.join(str(x) for x in list(empty_relations)[:20]), - ", ..." if len(empty_relations) > 20 else "") + logging.warning( + "Empty relations (%d in total): %s%s", + len(empty_relations), + ', '.join(str(x) for x in list(empty_relations)[:20]), + ", ..." if len(empty_relations) > 20 else "", + ) if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument( - '-i', '--source', help='File to write backup of OSM data, or to read data from') - parser.add_argument('-x', '--xml', help='OSM extract with routes, to read data from') - parser.add_argument('--overpass-api', - default='http://overpass-api.de/api/interpreter', - help="Overpass API URL") - parser.add_argument('-q', '--quiet', action='store_true', help='Show only warnings and errors') - parser.add_argument('-c', '--city', help='Validate only a single city or a country') - parser.add_argument('-t', '--overground', action='store_true', - help='Process overground transport instead of subways') - parser.add_argument('-e', '--entrances', type=argparse.FileType('w', encoding='utf-8'), - help='Export unused subway entrances as GeoJSON here') - parser.add_argument('-l', '--log', type=argparse.FileType('w', encoding='utf-8'), - help='Validation JSON file name') - parser.add_argument('-o', '--output', type=argparse.FileType('w', encoding='utf-8'), - help='Processed metro systems output') + '-i', + '--source', + help='File to write backup of OSM data, or to read data from', + ) + parser.add_argument( + '-x', '--xml', help='OSM extract with routes, to read data from' + ) + parser.add_argument( + '--overpass-api', + default='http://overpass-api.de/api/interpreter', + help="Overpass API URL", + ) + parser.add_argument( + '-q', + '--quiet', + action='store_true', + help='Show only warnings and errors', + ) + parser.add_argument( + '-c', '--city', help='Validate only a single city or a country' + ) + parser.add_argument( + '-t', + '--overground', + action='store_true', + help='Process overground transport instead of subways', + ) + parser.add_argument( + '-e', + '--entrances', + type=argparse.FileType('w', encoding='utf-8'), + help='Export unused subway entrances as GeoJSON here', + ) + parser.add_argument( + '-l', + '--log', + type=argparse.FileType('w', encoding='utf-8'), + help='Validation JSON file name', + ) + parser.add_argument( + '-o', + '--output', + type=argparse.FileType('w', encoding='utf-8'), + help='Processed metro systems output', + ) parser.add_argument('--cache', help='Cache file name for processed data') - parser.add_argument('-r', '--recovery-path', help='Cache file name for error recovery') - parser.add_argument('-d', '--dump', help='Make a YAML file for a city data') - parser.add_argument('-j', '--geojson', help='Make a GeoJSON file for a city data') - parser.add_argument('--crude', action='store_true', - help='Do not use OSM railway geometry for GeoJSON') + parser.add_argument( + '-r', '--recovery-path', help='Cache file name for error recovery' + ) + parser.add_argument( + '-d', '--dump', help='Make a YAML file for a city data' + ) + parser.add_argument( + '-j', '--geojson', help='Make a GeoJSON file for a city data' + ) + parser.add_argument( + '--crude', + action='store_true', + help='Do not use OSM railway geometry for GeoJSON', + ) options = parser.parse_args() if options.quiet: log_level = logging.WARNING else: log_level = logging.INFO - logging.basicConfig(level=log_level, datefmt='%H:%M:%S', - format='%(asctime)s %(levelname)-7s %(message)s') + logging.basicConfig( + level=log_level, + datefmt='%H:%M:%S', + format='%(asctime)s %(levelname)-7s %(message)s', + ) # Downloading cities from Google Spreadsheets cities = download_cities(options.overground) if options.city: - cities = [c for c in cities if c.name == options.city or c.country == options.city] + cities = [ + c + for c in cities + if c.name == options.city or c.country == options.city + ] if not cities: logging.error('No cities to process') sys.exit(2) @@ -223,8 +285,10 @@ if __name__ == '__main__': json.dump(osm, f) else: if len(cities) > 10: - logging.error('Would not download that many cities from Overpass API, ' - 'choose a smaller set') + logging.error( + 'Would not download that many cities from Overpass API, ' + 'choose a smaller set' + ) sys.exit(3) bboxes = [c.bbox for c in cities] logging.info('Downloading data from Overpass API') @@ -247,10 +311,18 @@ if __name__ == '__main__': try: c.extract_routes() except CriticalValidationError as e: - logging.error("Critical validation error while processing %s: %s", c.name, str(e)) + logging.error( + "Critical validation error while processing %s: %s", + c.name, + str(e), + ) c.error(str(e)) except AssertionError as e: - logging.error("Validation logic error while processing %s: %s", c.name, str(e)) + logging.error( + "Validation logic error while processing %s: %s", + c.name, + str(e), + ) c.error("Validation logic error: {}".format(str(e))) else: c.validate() @@ -261,11 +333,17 @@ if __name__ == '__main__': transfers = find_transfers(osm, cities) good_city_names = set(c.name for c in good_cities) - logging.info('%s good cities: %s', len(good_city_names), - ', '.join(sorted(good_city_names))) + logging.info( + '%s good cities: %s', + len(good_city_names), + ', '.join(sorted(good_city_names)), + ) bad_city_names = set(c.name for c in cities) - good_city_names - logging.info('%s bad cities: %s', len(bad_city_names), - ', '.join(sorted(bad_city_names))) + logging.info( + '%s bad cities: %s', + len(bad_city_names), + ', '.join(sorted(bad_city_names)), + ) if options.recovery_path: write_recovery_data(options.recovery_path, recovery_data, cities) @@ -276,8 +354,11 @@ if __name__ == '__main__': if options.dump: if os.path.isdir(options.dump): for c in cities: - with open(os.path.join(options.dump, slugify(c.name) + '.yaml'), - 'w', encoding='utf-8') as f: + with open( + os.path.join(options.dump, slugify(c.name) + '.yaml'), + 'w', + encoding='utf-8', + ) as f: dump_yaml(c, f) elif len(cities) == 1: with open(options.dump, 'w', encoding='utf-8') as f: @@ -288,14 +369,21 @@ if __name__ == '__main__': if options.geojson: if os.path.isdir(options.geojson): for c in cities: - with open(os.path.join(options.geojson, slugify(c.name) + '.geojson'), - 'w', encoding='utf-8') as f: + with open( + os.path.join( + options.geojson, slugify(c.name) + '.geojson' + ), + 'w', + encoding='utf-8', + ) as f: json.dump(make_geojson(c, not options.crude), f) elif len(cities) == 1: with open(options.geojson, 'w', encoding='utf-8') as f: json.dump(make_geojson(cities[0], not options.crude), f) else: - logging.error('Cannot make a geojson of %s cities at once', len(cities)) + logging.error( + 'Cannot make a geojson of %s cities at once', len(cities) + ) if options.log: res = [] @@ -306,5 +394,9 @@ if __name__ == '__main__': json.dump(res, options.log, indent=2, ensure_ascii=False) if options.output: - json.dump(processor.process(cities, transfers, options.cache), - options.output, indent=1, ensure_ascii=False) + 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 9855b27..c33c97f 100755 --- a/processors/mapsme.py +++ b/processors/mapsme.py @@ -3,15 +3,17 @@ import os import logging from collections import defaultdict from subway_structure import ( - distance, el_center, Station, - DISPLACEMENT_TOLERANCE + distance, + el_center, + Station, + DISPLACEMENT_TOLERANCE, ) OSM_TYPES = {'n': (0, 'node'), 'w': (2, 'way'), 'r': (3, 'relation')} ENTRANCE_PENALTY = 60 # seconds TRANSFER_PENALTY = 30 # seconds -KMPH_TO_MPS = 1/3.6 # km/h to m/s conversion multiplier +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 @@ -37,18 +39,22 @@ class DummyCache: 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 @@ -66,8 +72,11 @@ class MapsmeCache: 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) + 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 @@ -80,15 +89,20 @@ class MapsmeCache: """ 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']) + 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)): + 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')) + cached_station_coords = tuple( + cached_stoparea[coord] for coord in ('lon', 'lat') + ) displacement = distance(station_coords, cached_station_coords) - if displacement > DISPLACEMENT_TOLERANCE: + if displacement > DISPLACEMENT_TOLERANCE: return False return True @@ -123,7 +137,7 @@ class MapsmeCache: self.cache[city_name] = { 'network': network, 'stops': {}, # stoparea el_id -> jsonified stop data - 'transfers': [] # list of tuples (stoparea1_uid, stoparea2_uid, time); uid1 < uid2 + 'transfers': [], # list of tuples (stoparea1_uid, stoparea2_uid, time); uid1 < uid2 } @if_object_is_used @@ -142,9 +156,11 @@ class MapsmeCache: @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]): + 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) ) @@ -186,7 +202,6 @@ def process(cities, transfers, cache_path): exits.append(n) return exits - cache = MapsmeCache(cache_path, cities) stop_areas = {} # stoparea el_id -> StopArea instance @@ -206,7 +221,7 @@ def process(cities, transfers, cache_path): 'name': route.name, 'colour': format_colour(route.colour), 'route_id': uid(route.id, 'r'), - 'itineraries': [] + 'itineraries': [], } if route.infill: routes['casing'] = routes['colour'] @@ -216,33 +231,62 @@ def process(cities, transfers, cache_path): for stop in variant: 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)]) + 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: + if ( + len(stop.stoparea.entrances) + len(stop.stoparea.exits) + == 0 + ): for pl in stop.stoparea.platforms: pl_el = city.elements[pl] if pl_el['type'] == 'node': pl_nodes = [pl_el] elif pl_el['type'] == 'way': - pl_nodes = [city.elements.get('n{}'.format(n)) - for n in pl_el['nodes']] + pl_nodes = [ + city.elements.get('n{}'.format(n)) + for n in pl_el['nodes'] + ] else: pl_nodes = [] for m in pl_el['members']: if m['type'] == 'way': - if '{}{}'.format(m['type'][0], m['ref']) in city.elements: + if ( + '{}{}'.format( + m['type'][0], m['ref'] + ) + in city.elements + ): pl_nodes.extend( - [city.elements.get('n{}'.format(n)) - for n in city.elements['{}{}'.format( - m['type'][0], m['ref'])]['nodes']]) + [ + city.elements.get( + 'n{}'.format(n) + ) + for n in city.elements[ + '{}{}'.format( + m['type'][0], + m['ref'], + ) + ]['nodes'] + ] + ) pl_nodes = [n for n in pl_nodes if n] platform_nodes[pl] = find_exits_for_platform( - stop.stoparea.centers[pl], pl_nodes) + stop.stoparea.centers[pl], pl_nodes + ) - routes['itineraries'].append({ - 'stops': itin, - 'interval': round((variant.interval or DEFAULT_INTERVAL) * 60) - }) + routes['itineraries'].append( + { + 'stops': itin, + 'interval': round( + (variant.interval or DEFAULT_INTERVAL) * 60 + ), + } + ) network['routes'].append(routes) networks.append(network) @@ -261,41 +305,57 @@ def process(cities, transfers, cache_path): for e_l, k in ((stop.entrances, 'entrances'), (stop.exits, 'exits')): for e in e_l: if e[0] == 'n': - st[k].append({ - 'osm_type': 'node', - 'osm_id': int(e[1:]), - 'lon': stop.centers[e][0], - 'lat': stop.centers[e][1], - 'distance': ENTRANCE_PENALTY + round(distance( - stop.centers[e], stop.center)/SPEED_TO_ENTRANCE) - }) + st[k].append( + { + 'osm_type': 'node', + 'osm_id': int(e[1:]), + 'lon': stop.centers[e][0], + 'lat': stop.centers[e][1], + 'distance': ENTRANCE_PENALTY + + round( + distance(stop.centers[e], stop.center) + / SPEED_TO_ENTRANCE + ), + } + ) if len(stop.entrances) + len(stop.exits) == 0: if stop.platforms: for pl in stop.platforms: for n in platform_nodes[pl]: for k in ('entrances', 'exits'): - st[k].append({ - 'osm_type': n['type'], - 'osm_id': n['id'], - 'lon': n['lon'], - 'lat': n['lat'], - 'distance': ENTRANCE_PENALTY + round(distance( - (n['lon'], n['lat']), stop.center)/SPEED_TO_ENTRANCE) - }) + st[k].append( + { + 'osm_type': n['type'], + 'osm_id': n['id'], + 'lon': n['lon'], + 'lat': n['lat'], + 'distance': ENTRANCE_PENALTY + + round( + distance( + (n['lon'], n['lat']), stop.center + ) + / SPEED_TO_ENTRANCE + ), + } + ) else: for k in ('entrances', 'exits'): - st[k].append({ - 'osm_type': OSM_TYPES[stop.station.id[0]][1], - 'osm_id': int(stop.station.id[1:]), - 'lon': stop.centers[stop.id][0], - 'lat': stop.centers[stop.id][1], - 'distance': 60 - }) + st[k].append( + { + 'osm_type': OSM_TYPES[stop.station.id[0]][1], + 'osm_id': int(stop.station.id[1:]), + 'lon': stop.centers[stop.id][0], + 'lat': stop.centers[stop.id][1], + 'distance': 60, + } + ) stops[stop_id] = st cache.add_stop(stop_id, st) - pairwise_transfers = {} # (stoparea1_uid, stoparea2_uid) -> time; uid1 < uid2 + 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): @@ -306,23 +366,24 @@ def process(cities, transfers, cache_path): 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)) + 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) 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()] + pairwise_transfers = [ + (stop1_uid, stop2_uid, transfer_time) + for (stop1_uid, stop2_uid), transfer_time in pairwise_transfers.items() + ] result = { 'stops': list(stops.values()), 'transfers': pairwise_transfers, - 'networks': networks + 'networks': networks, } return result diff --git a/scripts/process_subways.sh b/scripts/process_subways.sh index 8a02442..b0d5f3b 100755 --- a/scripts/process_subways.sh +++ b/scripts/process_subways.sh @@ -19,7 +19,7 @@ In more detail, the script does the following: - Copies results onto remote server, if it is set up. During this procedure, as many steps are skipped as possible. Namely: - - Making metro extract is skipped if \$PLANET_METRO variable is set and the file exists. + - Generation of metro extract is skipped if \$PLANET_METRO variable is set and the file exists. - Update with osmupdate is skipped if \$SKIP_PLANET_UPDATE or \$SKIP_FILTERING is set. - Filtering is skipped if \$SKIP_FILTERING is set and \$FILTERED_DATA is set and the file exists. diff --git a/stop_areas/make_stop_areas.py b/stop_areas/make_stop_areas.py index 9f059b9..43699a9 100755 --- a/stop_areas/make_stop_areas.py +++ b/stop_areas/make_stop_areas.py @@ -54,17 +54,21 @@ class StationWrapper: def distance(self, other): """Calculate distance in meters.""" dx = math.radians(self[0] - other['lon']) * math.cos( - 0.5 * math.radians(self[1] + other['lat'])) + 0.5 * math.radians(self[1] + other['lat']) + ) dy = math.radians(self[1] - other['lat']) - return 6378137 * math.sqrt(dx*dx + dy*dy) + return 6378137 * math.sqrt(dx * dx + dy * dy) def overpass_request(bbox): url = 'http://overpass-api.de/api/interpreter?data={}'.format( - urllib.parse.quote(QUERY.replace('{{bbox}}', bbox))) + urllib.parse.quote(QUERY.replace('{{bbox}}', bbox)) + ) response = urllib.request.urlopen(url, timeout=1000) if response.getcode() != 200: - raise Exception('Failed to query Overpass API: HTTP {}'.format(response.getcode())) + raise Exception( + 'Failed to query Overpass API: HTTP {}'.format(response.getcode()) + ) reader = codecs.getreader('utf-8') return json.load(reader(response))['elements'] @@ -80,8 +84,11 @@ def add_stop_areas(src): stations[el_id(el)] = el for el in src: - if (el['type'] == 'relation' and 'tags' in el and - el['tags'].get('route', None) in ('subway', 'light_rail')): + if ( + el['type'] == 'relation' + and 'tags' in el + and el['tags'].get('route', None) in ('subway', 'light_rail') + ): for m in el['members']: st = stations.get(el_id(m), None) if st and 'station' not in st['tags']: @@ -91,7 +98,10 @@ def add_stop_areas(src): # Create a kd-tree out of subway stations stations = kdtree.create(dimensions=2) for el in src: - if 'tags' in el and el['tags'].get('station', None) in ('subway', 'light_rail'): + if 'tags' in el and el['tags'].get('station', None) in ( + 'subway', + 'light_rail', + ): stations.add(StationWrapper(el)) if stations.is_leaf: @@ -105,13 +115,21 @@ def add_stop_areas(src): continue if 'station' in el['tags']: continue - if (el['tags'].get('railway', None) not in ('subway_entrance', 'platform') and - el['tags'].get('public_transport', None) not in ('platform', 'stop_position')): + if el['tags'].get('railway', None) not in ( + 'subway_entrance', + 'platform', + ) and el['tags'].get('public_transport', None) not in ( + 'platform', + 'stop_position', + ): continue coords = el.get('center', el) station = stations.search_nn((coords['lon'], coords['lat']))[0].data if station.distance(coords) < MAX_DISTANCE: - k = (station.station['id'], station.station['tags'].get('name', 'station_with_no_name')) + k = ( + station.station['id'], + station.station['tags'].get('name', 'station_with_no_name'), + ) # Disregard exits and platforms that are differently named if el['tags'].get('name', k[1]) == k[1]: if k not in stop_areas: @@ -120,7 +138,10 @@ def add_stop_areas(src): # Find existing stop_area relations for stations and remove these stations for el in src: - if el['type'] == 'relation' and el['tags'].get('public_transport', None) == 'stop_area': + if ( + el['type'] == 'relation' + and el['tags'].get('public_transport', None) == 'stop_area' + ): found = False for m in el['members']: if found: @@ -141,18 +162,35 @@ def add_stop_areas(src): etree.SubElement(rel, 'tag', k='public_transport', v='stop_area') etree.SubElement(rel, 'tag', k='name', v=st[1]) for m in members.values(): - if m['tags'].get('railway', m['tags'].get('public_transport', None)) == 'platform': + if ( + m['tags'].get( + 'railway', m['tags'].get('public_transport', None) + ) + == 'platform' + ): role = 'platform' elif m['tags'].get('public_transport', None) == 'stop_position': role = 'stop' else: role = '' - etree.SubElement(rel, 'member', ref=str(m['id']), type=m['type'], role=role) + etree.SubElement( + rel, 'member', ref=str(m['id']), type=m['type'], role=role + ) # Add all downloaded elements for el in src: obj = etree.SubElement(root, el['type']) - for a in ('id', 'type', 'user', 'uid', 'version', 'changeset', 'timestamp', 'lat', 'lon'): + for a in ( + 'id', + 'type', + 'user', + 'uid', + 'version', + 'changeset', + 'timestamp', + 'lat', + 'lon', + ): if a in el: obj.set(a, str(el[a])) if 'modified' in el: @@ -162,8 +200,13 @@ def add_stop_areas(src): etree.SubElement(obj, 'tag', k=k, v=v) if 'members' in el: for m in el['members']: - etree.SubElement(obj, 'member', ref=str(m['ref']), - type=m['type'], role=m.get('role', '')) + etree.SubElement( + obj, + 'member', + ref=str(m['ref']), + type=m['type'], + role=m.get('role', ''), + ) if 'nodes' in el: for n in el['nodes']: etree.SubElement(obj, 'nd', ref=str(n)) @@ -173,8 +216,14 @@ def add_stop_areas(src): if __name__ == '__main__': if len(sys.argv) < 2: - print('Read a JSON from Overpass and output JOSM OSM XML with added stop_area relations') - print('Usage: {} {{|}} [output.osm]'.format(sys.argv[0])) + print( + 'Read a JSON from Overpass and output JOSM OSM XML with added stop_area relations' + ) + print( + 'Usage: {} {{|}} [output.osm]'.format( + sys.argv[0] + ) + ) sys.exit(1) if re.match(r'^[-0-9.,]+$', sys.argv[1]): diff --git a/stop_areas/make_tram_areas.py b/stop_areas/make_tram_areas.py index 443faba..f06fdac 100755 --- a/stop_areas/make_tram_areas.py +++ b/stop_areas/make_tram_areas.py @@ -45,17 +45,21 @@ class StationWrapper: def distance(self, other): """Calculate distance in meters.""" dx = math.radians(self[0] - other['lon']) * math.cos( - 0.5 * math.radians(self[1] + other['lat'])) + 0.5 * math.radians(self[1] + other['lat']) + ) dy = math.radians(self[1] - other['lat']) - return 6378137 * math.sqrt(dx*dx + dy*dy) + return 6378137 * math.sqrt(dx * dx + dy * dy) def overpass_request(bbox): url = 'http://overpass-api.de/api/interpreter?data={}'.format( - urllib.parse.quote(QUERY.replace('{{bbox}}', bbox))) + urllib.parse.quote(QUERY.replace('{{bbox}}', bbox)) + ) response = urllib.request.urlopen(url, timeout=1000) if response.getcode() != 200: - raise Exception('Failed to query Overpass API: HTTP {}'.format(response.getcode())) + raise Exception( + 'Failed to query Overpass API: HTTP {}'.format(response.getcode()) + ) reader = codecs.getreader('utf-8') return json.load(reader(response))['elements'] @@ -91,7 +95,11 @@ def add_stop_areas(src): stop_areas = {} for el in src: # Only tram routes - if 'tags' not in el or el['type'] != 'relation' or el['tags'].get('route') != 'tram': + if ( + 'tags' not in el + or el['type'] != 'relation' + or el['tags'].get('route') != 'tram' + ): continue for m in el['members']: if el_id(m) not in elements: @@ -102,16 +110,24 @@ def add_stop_areas(src): if pel['tags'].get('railway') == 'tram_stop': continue coords = pel.get('center', pel) - station = stations.search_nn((coords['lon'], coords['lat']))[0].data + station = stations.search_nn( + (coords['lon'], coords['lat']) + )[0].data if station.distance(coords) < MAX_DISTANCE: - k = (station.station['id'], station.station['tags'].get('name', None)) + k = ( + station.station['id'], + station.station['tags'].get('name', None), + ) if k not in stop_areas: stop_areas[k] = {el_id(station.station): station.station} stop_areas[k][el_id(m)] = pel # Find existing stop_area relations for stations and remove these stations for el in src: - if el['type'] == 'relation' and el['tags'].get('public_transport', None) == 'stop_area': + if ( + el['type'] == 'relation' + and el['tags'].get('public_transport', None) == 'stop_area' + ): found = False for m in el['members']: if found: @@ -133,12 +149,24 @@ def add_stop_areas(src): if st[1]: etree.SubElement(rel, 'tag', k='name', v=st[1]) for m in members.values(): - etree.SubElement(rel, 'member', ref=str(m['id']), type=m['type'], role='') + etree.SubElement( + rel, 'member', ref=str(m['id']), type=m['type'], role='' + ) # Add all downloaded elements for el in src: obj = etree.SubElement(root, el['type']) - for a in ('id', 'type', 'user', 'uid', 'version', 'changeset', 'timestamp', 'lat', 'lon'): + for a in ( + 'id', + 'type', + 'user', + 'uid', + 'version', + 'changeset', + 'timestamp', + 'lat', + 'lon', + ): if a in el: obj.set(a, str(el[a])) if 'modified' in el: @@ -148,8 +176,13 @@ def add_stop_areas(src): etree.SubElement(obj, 'tag', k=k, v=v) if 'members' in el: for m in el['members']: - etree.SubElement(obj, 'member', ref=str(m['ref']), - type=m['type'], role=m.get('role', '')) + etree.SubElement( + obj, + 'member', + ref=str(m['ref']), + type=m['type'], + role=m.get('role', ''), + ) if 'nodes' in el: for n in el['nodes']: etree.SubElement(obj, 'nd', ref=str(n)) @@ -159,8 +192,15 @@ def add_stop_areas(src): if __name__ == '__main__': if len(sys.argv) < 2: - print('Read a JSON from Overpass and output JOSM OSM XML with added stop_area relations') - print('Usage: {} {{|}} [output.osm]'.format(sys.argv[0])) + print( + 'Read a JSON from Overpass and output JOSM OSM XML ' + 'with added stop_area relations' + ) + print( + 'Usage: {} {{|}} [output.osm]'.format( + sys.argv[0] + ) + ) sys.exit(1) if re.match(r'^[-0-9.,]+$', sys.argv[1]): diff --git a/stop_areas/serve.py b/stop_areas/serve.py index c6f4c49..e5d695e 100755 --- a/stop_areas/serve.py +++ b/stop_areas/serve.py @@ -18,8 +18,11 @@ def convert(): return 'No data from overpass, sorry.' result = add_stop_areas(src) response = make_response(result) - response.headers['Content-Disposition'] = 'attachment; filename="stop_areas.osm"' + response.headers['Content-Disposition'] = ( + 'attachment; filename="stop_areas.osm"' + ) return response + if __name__ == '__main__': app.run() diff --git a/subway_io.py b/subway_io.py index abf1aee..810c02e 100644 --- a/subway_io.py +++ b/subway_io.py @@ -26,9 +26,13 @@ def load_xml(f): elif sub.tag == 'nd': nd.append(int(sub.get('ref'))) elif sub.tag == 'member': - members.append({'type': sub.get('type'), - 'ref': int(sub.get('ref')), - 'role': sub.get('role', '')}) + members.append( + { + 'type': sub.get('type'), + 'ref': int(sub.get('ref')), + 'role': sub.get('role', ''), + } + ) if tags: el['tags'] = tags if nd: @@ -44,13 +48,15 @@ def load_xml(f): _YAML_SPECIAL_CHARACTERS = "!&*{}[],#|>@`'\"" _YAML_SPECIAL_SEQUENCES = ("- ", ": ", "? ") + def _get_yaml_compatible_string(scalar): """Enclose string in single quotes in some cases""" string = str(scalar) - if (string and - (string[0] in _YAML_SPECIAL_CHARACTERS - or any(seq in string for seq in _YAML_SPECIAL_SEQUENCES) - or string.endswith(':'))): + if string and ( + string[0] in _YAML_SPECIAL_CHARACTERS + or any(seq in string for seq in _YAML_SPECIAL_SEQUENCES) + or string.endswith(':') + ): string = string.replace("'", "''") string = "'{}'".format(string) return string @@ -81,7 +87,9 @@ def dump_yaml(city, f): stops = set() routes = [] for route in city: - stations = OrderedDict([(sa.transfer or sa.id, sa.name) for sa in route.stop_areas()]) + stations = OrderedDict( + [(sa.transfer or sa.id, sa.name) for sa in route.stop_areas()] + ) rte = { 'type': route.mode, 'ref': route.ref, @@ -90,7 +98,7 @@ def dump_yaml(city, f): 'infill': route.infill, 'station_count': len(stations), 'stations': list(stations.values()), - 'itineraries': {} + 'itineraries': {}, } for variant in route: if INCLUDE_STOP_AREAS: @@ -98,14 +106,22 @@ def dump_yaml(city, f): for st in variant: s = st.stoparea if s.id == s.station.id: - v_stops.append('{} ({})'.format(s.station.name, s.station.id)) + v_stops.append( + '{} ({})'.format(s.station.name, s.station.id) + ) else: - v_stops.append('{} ({}) in {} ({})'.format(s.station.name, s.station.id, - s.name, s.id)) + v_stops.append( + '{} ({}) in {} ({})'.format( + s.station.name, s.station.id, s.name, s.id + ) + ) else: - v_stops = ['{} ({})'.format( - s.stoparea.station.name, - s.stoparea.station.id) for s in variant] + v_stops = [ + '{} ({})'.format( + s.stoparea.station.name, s.stoparea.station.id + ) + for s in variant + ] rte['itineraries'][variant.id] = v_stops stops.update(v_stops) routes.append(rte) @@ -132,64 +148,73 @@ def make_geojson(city, tracks=True): for rmaster in city: for variant in rmaster: if not tracks: - features.append({ - 'type': 'Feature', - 'geometry': { - 'type': 'LineString', - 'coordinates': [s.stop for s in variant], - }, - 'properties': { - 'ref': variant.ref, - 'name': variant.name, - 'stroke': variant.colour + features.append( + { + 'type': 'Feature', + 'geometry': { + 'type': 'LineString', + 'coordinates': [s.stop for s in variant], + }, + 'properties': { + 'ref': variant.ref, + 'name': variant.name, + 'stroke': variant.colour, + }, } - }) + ) elif variant.tracks: - features.append({ - 'type': 'Feature', - 'geometry': { - 'type': 'LineString', - 'coordinates': variant.tracks, - }, - 'properties': { - 'ref': variant.ref, - 'name': variant.name, - 'stroke': variant.colour + features.append( + { + 'type': 'Feature', + 'geometry': { + 'type': 'LineString', + 'coordinates': variant.tracks, + }, + 'properties': { + 'ref': variant.ref, + 'name': variant.name, + 'stroke': variant.colour, + }, } - }) + ) for st in variant: stops.add(st.stop) stopareas.add(st.stoparea) for stop in stops: - features.append({ - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': stop, - }, - 'properties': { - 'marker-size': 'small', - 'marker-symbol': 'circle' + features.append( + { + 'type': 'Feature', + 'geometry': { + 'type': 'Point', + 'coordinates': stop, + }, + 'properties': { + 'marker-size': 'small', + 'marker-symbol': 'circle', + }, } - }) + ) for stoparea in stopareas: - features.append({ - 'type': 'Feature', - 'geometry': { - 'type': 'Point', - 'coordinates': stoparea.center, - }, - 'properties': { - 'name': stoparea.name, - 'marker-size': 'small', - 'marker-color': '#ff2600' if stoparea in transfers else '#797979' + features.append( + { + 'type': 'Feature', + 'geometry': { + 'type': 'Point', + 'coordinates': stoparea.center, + }, + 'properties': { + 'name': stoparea.name, + 'marker-size': 'small', + 'marker-color': '#ff2600' + if stoparea in transfers + else '#797979', + }, } - }) + ) return {'type': 'FeatureCollection', 'features': features} - def _dumps_route_id(route_id): """Argument is a route_id that depends on route colour and ref. Name can be taken from route_master or can be route's own, we don't take it @@ -223,9 +248,11 @@ def read_recovery_data(path): return {} else: data = { - city_name: {_loads_route_id(route_id): route_data - for route_id, route_data in routes.items()} - for city_name, routes in data.items() + city_name: { + _loads_route_id(route_id): route_data + for route_id, route_data in routes.items() + } + for city_name, routes in data.items() } return data @@ -241,20 +268,24 @@ def write_recovery_data(path, current_data, cities): route_id = (route.colour, route.ref) itineraries = [] for variant in route: - itin = {'stations': [], - 'name': variant.name, - 'from': variant.element['tags'].get('from'), - 'to': variant.element['tags'].get('to')} + itin = { + 'stations': [], + 'name': variant.name, + 'from': variant.element['tags'].get('from'), + 'to': variant.element['tags'].get('to'), + } for stop in variant: station = stop.stoparea.station station_name = station.name if station_name == '?' and station.int_name: station_name = station.int_name - itin['stations'].append({ - 'oms_id': station.id, - 'name': station_name, - 'center': station.center - }) + itin['stations'].append( + { + 'oms_id': station.id, + 'name': station_name, + 'center': station.center, + } + ) if itin is not None: itineraries.append(itin) routes[route_id] = itineraries @@ -267,12 +298,13 @@ def write_recovery_data(path, current_data, cities): try: data = { - city_name: {_dumps_route_id(route_id): route_data - for route_id, route_data in routes.items()} + city_name: { + _dumps_route_id(route_id): route_data + for route_id, route_data in routes.items() + } for city_name, routes in data.items() } with open(path, 'w', encoding='utf-8') as f: json.dump(data, f, ensure_ascii=False, indent=2) except Exception as e: logging.warning("Cannot write recovery data to '%s': %s", path, str(e)) - diff --git a/subway_structure.py b/subway_structure.py index c5fc18e..222a9db 100644 --- a/subway_structure.py +++ b/subway_structure.py @@ -10,7 +10,7 @@ from collections import Counter, defaultdict SPREADSHEET_ID = '1SEW1-NiNOnA2qDwievcxYV1FOaQl1mb1fdeyqAxHu3k' MAX_DISTANCE_TO_ENTRANCES = 300 # in meters MAX_DISTANCE_STOP_TO_LINE = 50 # in meters -ALLOWED_STATIONS_MISMATCH = 0.02 # part of total station count +ALLOWED_STATIONS_MISMATCH = 0.02 # part of total station count ALLOWED_TRANSFERS_MISMATCH = 0.07 # part of total interchanges count ALLOWED_ANGLE_BETWEEN_STOPS = 45 # in degrees DISALLOWED_ANGLE_BETWEEN_STOPS = 20 # in degrees @@ -24,9 +24,23 @@ MODES_OVERGROUND = set(('tram', 'bus', 'trolleybus', 'aerialway', 'ferry')) DEFAULT_MODES_RAPID = set(('subway', 'light_rail')) DEFAULT_MODES_OVERGROUND = set(('tram',)) # TODO: bus and trolleybus? ALL_MODES = MODES_RAPID | MODES_OVERGROUND -RAILWAY_TYPES = set(('rail', 'light_rail', 'subway', 'narrow_gauge', - 'funicular', 'monorail', 'tram')) -CONSTRUCTION_KEYS = ('construction', 'proposed', 'construction:railway', 'proposed:railway') +RAILWAY_TYPES = set( + ( + 'rail', + 'light_rail', + 'subway', + 'narrow_gauge', + 'funicular', + 'monorail', + 'tram', + ) +) +CONSTRUCTION_KEYS = ( + 'construction', + 'proposed', + 'construction:railway', + 'proposed:railway', +) used_entrances = set() @@ -56,27 +70,32 @@ def el_center(el): def distance(p1, p2): if p1 is None or p2 is None: - raise Exception('One of arguments to distance({}, {}) is None'.format(p1, p2)) + raise Exception( + 'One of arguments to distance({}, {}) is None'.format(p1, p2) + ) dx = math.radians(p1[0] - p2[0]) * math.cos( - 0.5 * math.radians(p1[1] + p2[1])) + 0.5 * math.radians(p1[1] + p2[1]) + ) dy = math.radians(p1[1] - p2[1]) - return 6378137 * math.sqrt(dx*dx + dy*dy) + return 6378137 * math.sqrt(dx * dx + dy * dy) def is_near(p1, p2): - return (p1[0] - 1e-8 <= p2[0] <= p1[0] + 1e-8 and - p1[1] - 1e-8 <= p2[1] <= p1[1] + 1e-8) + return ( + p1[0] - 1e-8 <= p2[0] <= p1[0] + 1e-8 + and p1[1] - 1e-8 <= p2[1] <= p1[1] + 1e-8 + ) def project_on_line(p, line): def project_on_segment(p, p1, p2): dp = (p2[0] - p1[0], p2[1] - p1[1]) - d2 = dp[0]*dp[0] + dp[1]*dp[1] + d2 = dp[0] * dp[0] + dp[1] * dp[1] if d2 < 1e-14: return None # u is the position of projection of p point on line p1p2 # regarding point p1 and (p2-p1) direction vector - u = ((p[0] - p1[0])*dp[0] + (p[1] - p1[1])*dp[1]) / d2 + u = ((p[0] - p1[0]) * dp[0] + (p[1] - p1[1]) * dp[1]) / d2 if not 0 <= u <= 1: return None return u @@ -87,7 +106,7 @@ def project_on_line(p, line): # the projected point lies on a segment between two vertices. More than # one value can occur if a route follows the same tracks more than once. 'positions_on_line': None, - 'projected_point': None # (lon, lat) + 'projected_point': None, # (lon, lat) } if len(line) < 2: @@ -106,18 +125,28 @@ def project_on_line(p, line): # Repeated occurrence of the track vertex in line, like Oslo Line 5 result['positions_on_line'].append(i) # And then calculate distances to each segment - for seg in range(len(line)-1): + for seg in range(len(line) - 1): # Check bbox for speed - if not ((min(line[seg][0], line[seg+1][0]) - MAX_DISTANCE_STOP_TO_LINE <= p[0] <= - max(line[seg][0], line[seg+1][0]) + MAX_DISTANCE_STOP_TO_LINE) and - (min(line[seg][1], line[seg+1][1]) - MAX_DISTANCE_STOP_TO_LINE <= p[1] <= - max(line[seg][1], line[seg+1][1]) + MAX_DISTANCE_STOP_TO_LINE)): + if not ( + ( + min(line[seg][0], line[seg + 1][0]) - MAX_DISTANCE_STOP_TO_LINE + <= p[0] + <= max(line[seg][0], line[seg + 1][0]) + + MAX_DISTANCE_STOP_TO_LINE + ) + and ( + min(line[seg][1], line[seg + 1][1]) - MAX_DISTANCE_STOP_TO_LINE + <= p[1] + <= max(line[seg][1], line[seg + 1][1]) + + MAX_DISTANCE_STOP_TO_LINE + ) + ): continue - u = project_on_segment(p, line[seg], line[seg+1]) + u = project_on_segment(p, line[seg], line[seg + 1]) if u: projected_point = ( - line[seg][0] + u * (line[seg+1][0] - line[seg][0]), - line[seg][1] + u * (line[seg+1][1] - line[seg][1]) + line[seg][0] + u * (line[seg + 1][0] - line[seg][0]), + line[seg][1] + u * (line[seg + 1][1] - line[seg][1]), ) d = distance(p, projected_point) if d < d_min: @@ -135,24 +164,24 @@ def project_on_line(p, line): def find_segment(p, line, start_vertex=0): """Returns index of a segment and a position inside it.""" EPS = 1e-9 - for seg in range(start_vertex, len(line)-1): + for seg in range(start_vertex, len(line) - 1): if is_near(p, line[seg]): return seg, 0 - if line[seg][0] == line[seg+1][0]: - if not (p[0]-EPS <= line[seg][0] <= p[0]+EPS): + if line[seg][0] == line[seg + 1][0]: + if not (p[0] - EPS <= line[seg][0] <= p[0] + EPS): continue px = None else: - px = (p[0] - line[seg][0]) / (line[seg+1][0] - line[seg][0]) + px = (p[0] - line[seg][0]) / (line[seg + 1][0] - line[seg][0]) if px is None or (0 <= px <= 1): - if line[seg][1] == line[seg+1][1]: - if not (p[1]-EPS <= line[seg][1] <= p[1]+EPS): + if line[seg][1] == line[seg + 1][1]: + if not (p[1] - EPS <= line[seg][1] <= p[1] + EPS): continue py = None else: - py = (p[1] - line[seg][1]) / (line[seg+1][1] - line[seg][1]) + py = (p[1] - line[seg][1]) / (line[seg + 1][1] - line[seg][1]) if py is None or (0 <= py <= 1): - if py is None or px is None or (px-EPS <= py <= px+EPS): + if py is None or px is None or (px - EPS <= py <= px + EPS): return seg, px or py return None, None @@ -176,24 +205,30 @@ def distance_on_line(p1, p2, line, start_vertex=0): # logging.warn('p2 %s is not projected, st=%s', p2, start_vertex) return None if seg1 == seg2: - return distance(line[seg1], line[seg1+1]) * abs(pos2-pos1), seg1 + return distance(line[seg1], line[seg1 + 1]) * abs(pos2 - pos1), seg1 if seg2 < seg1: # Should not happen raise Exception('Pos1 %s is after pos2 %s', seg1, seg2) d = 0 if pos1 < 1: - d += distance(line[seg1], line[seg1+1]) * (1-pos1) - for i in range(seg1+1, seg2): - d += distance(line[i], line[i+1]) + d += distance(line[seg1], line[seg1 + 1]) * (1 - pos1) + for i in range(seg1 + 1, seg2): + d += distance(line[i], line[i + 1]) if pos2 > 0: - d += distance(line[seg2], line[seg2+1]) * pos2 + d += distance(line[seg2], line[seg2 + 1]) * pos2 return d, seg2 % len(line_copy) def angle_between(p1, c, p2): - a = round(abs(math.degrees(math.atan2(p1[1]-c[1], p1[0]-c[0]) - - math.atan2(p2[1]-c[1], p2[0]-c[0])))) - return a if a <= 180 else 360-a + a = round( + abs( + math.degrees( + math.atan2(p1[1] - c[1], p1[0] - c[0]) + - math.atan2(p2[1] - c[1], p2[0] - c[0]) + ) + ) + ) + return a if a <= 180 else 360 - a def format_elid_list(ids): @@ -217,7 +252,10 @@ class Station: def is_station(el, modes): # public_transport=station is too ambiguous and unspecific to use, # so we expect for it to be backed by railway=station. - if 'tram' in modes and el.get('tags', {}).get('railway') == 'tram_stop': + if ( + 'tram' in modes + and el.get('tags', {}).get('railway') == 'tram_stop' + ): return True if el.get('tags', {}).get('railway') not in ('station', 'halt'): return False @@ -233,13 +271,17 @@ class Station: """Call this with a railway=station node.""" if not Station.is_station(el, city.modes): raise Exception( - 'Station object should be instantiated from a station node. Got: {}'.format(el)) + 'Station object should be instantiated from a station node. ' + 'Got: {}'.format(el) + ) self.id = el_id(el) self.element = el self.modes = Station.get_modes(el) self.name = el['tags'].get('name', '?') - self.int_name = el['tags'].get('int_name', el['tags'].get('name:en', None)) + self.int_name = el['tags'].get( + 'int_name', el['tags'].get('name:en', None) + ) try: self.colour = normalize_colour(el['tags'].get('colour', None)) except ValueError as e: @@ -251,7 +293,8 @@ class Station: def __repr__(self): return 'Station(id={}, modes={}, name={}, center={})'.format( - self.id, ','.join(self.modes), self.name, self.center) + self.id, ','.join(self.modes), self.name, self.center + ) class StopArea: @@ -287,13 +330,14 @@ class StopArea: self.element = stop_area or station.element self.id = el_id(self.element) self.station = station - self.stops = set() # set of el_ids of stop_positions + self.stops = set() # set of el_ids of stop_positions self.platforms = set() # set of el_ids of platforms - self.exits = set() # el_id of subway_entrance for leaving the platform - self.entrances = set() # el_id of subway_entrance for entering the platform - self.center = None # lon, lat of the station centre point - self.centers = {} # el_id -> (lon, lat) for all elements - self.transfer = None # el_id of a transfer relation + self.exits = set() # el_id of subway_entrance for leaving the platform + self.entrances = set() # el_id of subway_entrance for entering + # the platform + self.center = None # lon, lat of the station centre point + self.centers = {} # el_id -> (lon, lat) for all elements + self.transfer = None # el_id of a transfer relation self.modes = station.modes self.name = station.name @@ -303,10 +347,13 @@ class StopArea: if stop_area: self.name = stop_area['tags'].get('name', self.name) self.int_name = stop_area['tags'].get( - 'int_name', stop_area['tags'].get('name:en', self.int_name)) + 'int_name', stop_area['tags'].get('name:en', self.int_name) + ) try: - self.colour = normalize_colour( - stop_area['tags'].get('colour')) or self.colour + self.colour = ( + normalize_colour(stop_area['tags'].get('colour')) + or self.colour + ) except ValueError as e: city.warn(str(e), stop_area) @@ -318,7 +365,9 @@ class StopArea: if m_el and 'tags' in m_el: if Station.is_station(m_el, city.modes): if k != station.id: - city.error('Stop area has multiple stations', stop_area) + city.error( + 'Stop area has multiple stations', stop_area + ) elif StopArea.is_stop(m_el): self.stops.add(k) elif StopArea.is_platform(m_el): @@ -326,13 +375,21 @@ class StopArea: elif m_el['tags'].get('railway') == 'subway_entrance': if m_el['type'] != 'node': city.warn('Subway entrance is not a node', m_el) - if m_el['tags'].get('entrance') != 'exit' and m['role'] != 'exit_only': + if ( + m_el['tags'].get('entrance') != 'exit' + and m['role'] != 'exit_only' + ): self.entrances.add(k) - if m_el['tags'].get('entrance') != 'entrance' and m['role'] != 'entry_only': + if ( + m_el['tags'].get('entrance') != 'entrance' + and m['role'] != 'entry_only' + ): self.exits.add(k) elif StopArea.is_track(m_el): if not warned_about_tracks: - city.error('Tracks in a stop_area relation', stop_area) + city.error( + 'Tracks in a stop_area relation', stop_area + ) warned_about_tracks = True else: # Otherwise add nearby entrances @@ -342,9 +399,15 @@ class StopArea: c_id = el_id(c_el) if c_id not in city.stop_areas: c_center = el_center(c_el) - if c_center and distance(center, c_center) <= MAX_DISTANCE_TO_ENTRANCES: + if ( + c_center + and distance(center, c_center) + <= MAX_DISTANCE_TO_ENTRANCES + ): if c_el['type'] != 'node': - city.warn('Subway entrance is not a node', c_el) + city.warn( + 'Subway entrance is not a node', c_el + ) etag = c_el['tags'].get('entrance') if etag != 'exit': self.entrances.add(c_id) @@ -352,7 +415,10 @@ class StopArea: self.exits.add(c_id) if self.exits and not self.entrances: - city.error('Only exits for a station, no entrances', stop_area or station.element) + city.error( + 'Only exits for a station, no entrances', + stop_area or station.element, + ) if self.entrances and not self.exits: city.error('No exits for a station', stop_area or station.element) @@ -384,7 +450,8 @@ class StopArea: def __repr__(self): return 'StopArea(id={}, name={}, station={}, transfer={}, center={})'.format( - self.id, self.name, self.station, self.transfer, self.center) + self.id, self.name, self.station, self.transfer, self.center + ) class RouteStop: @@ -468,7 +535,12 @@ class RouteStop: self.seen_platform_exit = True else: if role != 'platform' and 'stop' not in role: - city.warn("Platform with invalid role '{}' in a route".format(role), el) + city.warn( + "Platform with invalid role '{}' in a route".format( + role + ), + el, + ) multiple_check = self.seen_platform self.seen_platform_entry = True self.seen_platform_exit = True @@ -479,18 +551,31 @@ class RouteStop: city.error_if( actual_role == 'stop', 'Multiple {}s for a station "{}" ({}) in a route relation'.format( - actual_role, el['tags'].get('name', ''), el_id(el)), relation) + actual_role, el['tags'].get('name', ''), el_id(el) + ), + relation, + ) def __repr__(self): - return 'RouteStop(stop={}, pl_entry={}, pl_exit={}, stoparea={})'.format( - self.stop, self.platform_entry, self.platform_exit, self.stoparea) + return ( + 'RouteStop(stop={}, pl_entry={}, pl_exit={}, stoparea={})'.format( + self.stop, + self.platform_entry, + self.platform_exit, + self.stoparea, + ) + ) class Route: """The longest route for a city with a unique ref.""" + @staticmethod def is_route(el, modes): - if el['type'] != 'relation' or el.get('tags', {}).get('type') != 'route': + if ( + el['type'] != 'relation' + or el.get('tags', {}).get('type') != 'route' + ): return False if 'members' not in el: return False @@ -519,7 +604,7 @@ class Route: break else: for kk in tags: - if kk.startswith(k+':'): + if kk.startswith(k + ':'): v = tags[kk] break if not v: @@ -554,7 +639,10 @@ class Route: track.extend(new_segment[1:]) elif new_segment[-1] == track[-1]: track.extend(reversed(new_segment[:-1])) - elif is_first and track[0] in (new_segment[0], new_segment[-1]): + elif is_first and track[0] in ( + new_segment[0], + new_segment[-1], + ): # We can reverse the track and try again track.reverse() if new_segment[0] == track[-1]: @@ -564,8 +652,12 @@ class Route: else: # Store the track if it is long and clean it if not warned_about_holes: - self.city.warn('Hole in route rails near node {}'.format( - track[-1]), relation) + self.city.warn( + 'Hole in route rails near node {}'.format( + track[-1] + ), + relation, + ) warned_about_holes = True if len(track) > len(last_track): last_track = track @@ -574,8 +666,11 @@ class Route: if len(track) > len(last_track): last_track = track # Remove duplicate points - last_track = [last_track[i] for i in range(0, len(last_track)) - if i == 0 or last_track[i-1] != last_track[i]] + last_track = [ + last_track[i] + for i in range(0, len(last_track)) + if i == 0 or last_track[i - 1] != last_track[i] + ] return last_track, line_nodes def project_stops_on_line(self): @@ -583,11 +678,12 @@ class Route: def is_stop_near_tracks(stop_index): return ( - projected[stop_index]['projected_point'] is not None and - distance( - self.stops[stop_index].stop, - projected[stop_index]['projected_point'] - ) <= MAX_DISTANCE_STOP_TO_LINE + projected[stop_index]['projected_point'] is not None + and distance( + self.stops[stop_index].stop, + projected[stop_index]['projected_point'], + ) + <= MAX_DISTANCE_STOP_TO_LINE ) start = 0 @@ -605,19 +701,29 @@ class Route: elif i > end: tracks_end.append(route_stop.stop) elif projected[i]['projected_point'] is None: - self.city.error('Stop "{}" {} is nowhere near the tracks'.format( - route_stop.stoparea.name, route_stop.stop), self.element) + self.city.error( + 'Stop "{}" {} is nowhere near the tracks'.format( + route_stop.stoparea.name, route_stop.stop + ), + self.element, + ) else: projected_point = projected[i]['projected_point'] # We've got two separate stations with a good stretch of # railway tracks between them. Put these on tracks. d = round(distance(route_stop.stop, projected_point)) if d > MAX_DISTANCE_STOP_TO_LINE: - self.city.warn('Stop "{}" {} is {} meters from the tracks'.format( - route_stop.stoparea.name, route_stop.stop, d), self.element) + self.city.warn( + 'Stop "{}" {} is {} meters from the tracks'.format( + route_stop.stoparea.name, route_stop.stop, d + ), + self.element, + ) else: route_stop.stop = projected_point - route_stop.positions_on_rails = projected[i]['positions_on_line'] + route_stop.positions_on_rails = projected[i][ + 'positions_on_line' + ] stops_on_longest_line.append(route_stop) if start >= len(self.stops): self.tracks = tracks_start @@ -630,9 +736,11 @@ class Route: vertex = 0 for i, stop in enumerate(self.stops): if i > 0: - direct = distance(stop.stop, self.stops[i-1].stop) - d_line = distance_on_line(self.stops[i-1].stop, stop.stop, self.tracks, vertex) - if d_line and direct-10 <= d_line[0] <= direct*2: + direct = distance(stop.stop, self.stops[i - 1].stop) + d_line = distance_on_line( + self.stops[i - 1].stop, stop.stop, self.tracks, vertex + ) + if d_line and direct - 10 <= d_line[0] <= direct * 2: vertex = d_line[1] dist += round(d_line[0]) else: @@ -641,46 +749,71 @@ class Route: def __init__(self, relation, city, master=None): if not Route.is_route(relation, city.modes): - raise Exception('The relation does not seem a route: {}'.format(relation)) + raise Exception( + 'The relation does not seem a route: {}'.format(relation) + ) master_tags = {} if not master else master['tags'] self.city = city self.element = relation self.id = el_id(relation) if 'ref' not in relation['tags'] and 'ref' not in master_tags: city.warn('Missing ref on a route', relation) - self.ref = relation['tags'].get('ref', master_tags.get( - 'ref', relation['tags'].get('name', None))) + self.ref = relation['tags'].get( + 'ref', master_tags.get('ref', relation['tags'].get('name', None)) + ) self.name = relation['tags'].get('name', None) self.mode = relation['tags']['route'] - if 'colour' not in relation['tags'] and 'colour' not in master_tags and self.mode != 'tram': + if ( + 'colour' not in relation['tags'] + and 'colour' not in master_tags + and self.mode != 'tram' + ): city.warn('Missing colour on a route', relation) try: - self.colour = normalize_colour(relation['tags'].get( - 'colour', master_tags.get('colour', None))) + self.colour = normalize_colour( + relation['tags'].get('colour', master_tags.get('colour', None)) + ) except ValueError as e: self.colour = None city.warn(str(e), relation) try: - self.infill = normalize_colour(relation['tags'].get( - 'colour:infill', master_tags.get('colour:infill', None))) + self.infill = normalize_colour( + relation['tags'].get( + 'colour:infill', master_tags.get('colour:infill', None) + ) + ) except ValueError as e: self.infill = None city.warn(str(e), relation) self.network = Route.get_network(relation) - self.interval = Route.get_interval(relation['tags']) or Route.get_interval(master_tags) + self.interval = Route.get_interval( + relation['tags'] + ) or Route.get_interval(master_tags) if relation['tags'].get('public_transport:version') == '1': - city.error('Public transport version is 1, which means the route ' - 'is an unsorted pile of objects', relation) + city.error( + 'Public transport version is 1, which means the route ' + 'is an unsorted pile of objects', + relation, + ) self.is_circular = False # self.tracks would be a list of (lon, lat) for the longest stretch. Can be empty tracks, line_nodes = self.build_longest_line(relation) self.tracks = [el_center(city.elements.get(k)) for k in tracks] - if None in self.tracks: # usually, extending BBOX for the city is needed + if ( + None in self.tracks + ): # usually, extending BBOX for the city is needed self.tracks = [] for n in filter(lambda x: x not in city.elements, tracks): - city.error('The dataset is missing the railway tracks node {}'.format(n), relation) + city.error( + 'The dataset is missing the railway tracks node {}'.format( + n + ), + relation, + ) break - check_stop_positions = len(line_nodes) > 50 # arbitrary number, of course + check_stop_positions = ( + len(line_nodes) > 50 + ) # arbitrary number, of course self.stops = [] # List of RouteStop stations = set() # temporary for recording stations seen_stops = False @@ -694,13 +827,23 @@ class Route: st_list = city.stations[k] st = st_list[0] if len(st_list) > 1: - city.error('Ambiguous station {} in route. Please use stop_position or split ' - 'interchange stations'.format(st.name), relation) + city.error( + 'Ambiguous station {} in route. Please use stop_position or split ' + 'interchange stations'.format(st.name), + relation, + ) el = city.elements[k] - actual_role = RouteStop.get_actual_role(el, m['role'], city.modes) + actual_role = RouteStop.get_actual_role( + el, m['role'], city.modes + ) if actual_role: if m['role'] and actual_role not in m['role']: - city.warn("Wrong role '{}' for {} {}".format(m['role'], actual_role, k), relation) + city.warn( + "Wrong role '{}' for {} {}".format( + m['role'], actual_role, k + ), + relation, + ) if repeat_pos is None: if not self.stops or st not in stations: stop = RouteStop(st) @@ -710,9 +853,17 @@ class Route: stop = self.stops[-1] else: # We've got a repeat - if ((seen_stops and seen_platforms) or - (actual_role == 'stop' and not seen_platforms) or - (actual_role == 'platform' and not seen_stops)): + if ( + (seen_stops and seen_platforms) + or ( + actual_role == 'stop' + and not seen_platforms + ) + or ( + actual_role == 'platform' + and not seen_stops + ) + ): # Circular route! stop = RouteStop(st) self.stops.append(stop) @@ -724,17 +875,28 @@ class Route: continue # Check that the type matches if (actual_role == 'stop' and seen_stops) or ( - actual_role == 'platform' and seen_platforms): - city.error('Found an out-of-place {}: "{}" ({})'.format( - actual_role, el['tags'].get('name', ''), k), relation) + actual_role == 'platform' and seen_platforms + ): + city.error( + 'Found an out-of-place {}: "{}" ({})'.format( + actual_role, el['tags'].get('name', ''), k + ), + relation, + ) continue # Find the matching stop starting with index repeat_pos - while (repeat_pos < len(self.stops) and - self.stops[repeat_pos].stoparea.id != st.id): + while ( + repeat_pos < len(self.stops) + and self.stops[repeat_pos].stoparea.id != st.id + ): repeat_pos += 1 if repeat_pos >= len(self.stops): - city.error('Incorrect order of {}s at {}'.format(actual_role, k), - relation) + city.error( + 'Incorrect order of {}s at {}'.format( + actual_role, k + ), + relation, + ) continue stop = self.stops[repeat_pos] @@ -745,8 +907,12 @@ class Route: if check_stop_positions and StopArea.is_stop(el): if k not in line_nodes: - city.warn('Stop position "{}" ({}) is not on tracks'.format( - el['tags'].get('name', ''), k), relation) + city.warn( + 'Stop position "{}" ({}) is not on tracks'.format( + el['tags'].get('name', ''), k + ), + relation, + ) continue if k not in city.elements: @@ -765,9 +931,13 @@ class Route: is_under_construction = False for ck in CONSTRUCTION_KEYS: if ck in el['tags']: - city.error('An under construction {} {} in route. Consider ' - 'setting \'inactive\' role or removing construction attributes' - .format(m['role'] or 'feature', k), relation) + city.error( + 'An under construction {} {} in route. Consider ' + 'setting \'inactive\' role or removing construction attributes'.format( + m['role'] or 'feature', k + ), + relation, + ) is_under_construction = True break if is_under_construction: @@ -778,20 +948,36 @@ class Route: # 'stop area has multiple stations' error. No other error message is needed. pass elif el['tags'].get('railway') in ('station', 'halt'): - city.error('Missing station={} on a {}'.format(self.mode, m['role']), el) + city.error( + 'Missing station={} on a {}'.format(self.mode, m['role']), + el, + ) else: - actual_role = RouteStop.get_actual_role(el, m['role'], city.modes) + actual_role = RouteStop.get_actual_role( + el, m['role'], city.modes + ) if actual_role: - city.error('{} {} {} is not connected to a station in route'.format( - actual_role, m['type'], m['ref']), relation) + city.error( + '{} {} {} is not connected to a station in route'.format( + actual_role, m['type'], m['ref'] + ), + relation, + ) elif not StopArea.is_track(el): - city.error('Unknown member type for {} {} in route'.format(m['type'], m['ref']), relation) + city.error( + 'Unknown member type for {} {} in route'.format( + m['type'], m['ref'] + ), + relation, + ) if not self.stops: city.error('Route has no stops', relation) elif len(self.stops) == 1: city.error('Route has only one stop', relation) else: - self.is_circular = self.stops[0].stoparea == self.stops[-1].stoparea + self.is_circular = ( + self.stops[0].stoparea == self.stops[-1].stoparea + ) stops_on_longest_line = self.project_stops_on_line() self.check_and_recover_stops_order(stops_on_longest_line) self.calculate_distances() @@ -800,12 +986,15 @@ class Route: disorder_warnings = [] disorder_errors = [] for si in range(len(self.stops) - 2): - angle = angle_between(self.stops[si].stop, - self.stops[si + 1].stop, - self.stops[si + 2].stop) + angle = angle_between( + self.stops[si].stop, + self.stops[si + 1].stop, + self.stops[si + 2].stop, + ) if angle < ALLOWED_ANGLE_BETWEEN_STOPS: msg = 'Angle between stops around "{}" is too narrow, {} degrees'.format( - self.stops[si + 1].stoparea.name, angle) + self.stops[si + 1].stoparea.name, angle + ) if angle < DISALLOWED_ANGLE_BETWEEN_STOPS: disorder_errors.append(msg) else: @@ -819,6 +1008,7 @@ class Route: longest contiguous sequence of tracks in a route. :return: error message on the first order violation or None. """ + def make_assertion_error_msg(route_stop, error_type): return ( "stop_area {} '{}' has {} 'positions_on_rails' " @@ -826,23 +1016,28 @@ class Route: route_stop.stoparea.id, route_stop.stoparea.name, "no" if error_type == 1 else "empty", - self.id + self.id, ) ) allowed_order_violations = 1 if self.is_circular else 0 max_position_on_rails = -1 for route_stop in stop_sequence: - assert hasattr(route_stop, 'positions_on_rails'), \ - make_assertion_error_msg(route_stop, error_type=1) + assert hasattr( + route_stop, 'positions_on_rails' + ), make_assertion_error_msg(route_stop, error_type=1) positions_on_rails = route_stop.positions_on_rails - assert positions_on_rails, \ - make_assertion_error_msg(route_stop, error_type=2) + assert positions_on_rails, make_assertion_error_msg( + route_stop, error_type=2 + ) suitable_occurrence = 0 - while (suitable_occurrence < len(positions_on_rails) and - positions_on_rails[suitable_occurrence] < max_position_on_rails): + while ( + suitable_occurrence < len(positions_on_rails) + and positions_on_rails[suitable_occurrence] + < max_position_on_rails + ): suitable_occurrence += 1 if suitable_occurrence == len(positions_on_rails): if allowed_order_violations > 0: @@ -850,13 +1045,12 @@ class Route: allowed_order_violations -= 1 else: return 'Stops on tracks are unordered near "{}" {}'.format( - route_stop.stoparea.name, - route_stop.stop + route_stop.stoparea.name, route_stop.stop ) max_position_on_rails = positions_on_rails[suitable_occurrence] def check_stops_order_on_tracks(self, stop_sequence): - """ Checks stops order on tracks, trying direct and reversed + """Checks stops order on tracks, trying direct and reversed order of stops in the stop_sequence. :param stop_sequence: list of RouteStop that belong to the longest contiguous sequence of tracks in a route. @@ -864,15 +1058,25 @@ class Route: """ error_message = self.check_stops_order_on_tracks_direct(stop_sequence) if error_message: - error_message_reversed = self.check_stops_order_on_tracks_direct(reversed(stop_sequence)) + error_message_reversed = self.check_stops_order_on_tracks_direct( + reversed(stop_sequence) + ) if error_message_reversed is None: error_message = None - self.city.warn('Tracks seem to go in the opposite direction to stops', self.element) + self.city.warn( + 'Tracks seem to go in the opposite direction to stops', + self.element, + ) return error_message def check_stops_order(self, stops_on_longest_line): - angle_disorder_warnings, angle_disorder_errors = self.check_stops_order_by_angle() - disorder_on_tracks_error = self.check_stops_order_on_tracks(stops_on_longest_line) + ( + angle_disorder_warnings, + angle_disorder_errors, + ) = self.check_stops_order_by_angle() + disorder_on_tracks_error = self.check_stops_order_on_tracks( + stops_on_longest_line + ) disorder_warnings = angle_disorder_warnings disorder_errors = angle_disorder_errors if disorder_on_tracks_error: @@ -880,7 +1084,9 @@ class Route: return disorder_warnings, disorder_errors def check_and_recover_stops_order(self, stops_on_longest_line): - disorder_warnings, disorder_errors = self.check_stops_order(stops_on_longest_line) + disorder_warnings, disorder_errors = self.check_stops_order( + stops_on_longest_line + ) if disorder_warnings or disorder_errors: resort_success = False if self.city.recovery_data: @@ -889,7 +1095,9 @@ class Route: for msg in disorder_warnings: self.city.warn(msg, self.element) for msg in disorder_errors: - self.city.warn("Fixed with recovery data: " + msg, self.element) + self.city.warn( + "Fixed with recovery data: " + msg, self.element + ) if not resort_success: for msg in disorder_warnings: @@ -900,7 +1108,7 @@ class Route: def try_resort_stops(self): """Precondition: self.city.recovery_data is not None. Return success of station order recovering.""" - self_stops = {} # station name => RouteStop + self_stops = {} # station name => RouteStop for stop in self.stops: station = stop.stoparea.station stop_name = station.name @@ -919,16 +1127,23 @@ class Route: stop_names = list(self_stops.keys()) suitable_itineraries = [] for itinerary in self.city.recovery_data[route_id]: - itinerary_stop_names = [stop['name'] for stop in itinerary['stations']] - if not (len(stop_names) == len(itinerary_stop_names) and - sorted(stop_names) == sorted(itinerary_stop_names)): + itinerary_stop_names = [ + stop['name'] for stop in itinerary['stations'] + ] + if not ( + len(stop_names) == len(itinerary_stop_names) + and sorted(stop_names) == sorted(itinerary_stop_names) + ): continue big_station_displacement = False for it_stop in itinerary['stations']: name = it_stop['name'] it_stop_center = it_stop['center'] self_stop_center = self_stops[name].stoparea.station.center - if distance(it_stop_center, self_stop_center) > DISPLACEMENT_TOLERANCE: + if ( + distance(it_stop_center, self_stop_center) + > DISPLACEMENT_TOLERANCE + ): big_station_displacement = True break if not big_station_displacement: @@ -944,14 +1159,19 @@ class Route: if not from_tag and not to_tag: return False matching_itineraries = [ - itin for itin in suitable_itineraries - if from_tag and itin['from'] == from_tag or - to_tag and itin['to'] == to_tag + itin + for itin in suitable_itineraries + if from_tag + and itin['from'] == from_tag + or to_tag + and itin['to'] == to_tag ] if len(matching_itineraries) != 1: return False matching_itinerary = matching_itineraries[0] - self.stops = [self_stops[stop['name']] for stop in matching_itinerary['stations']] + self.stops = [ + self_stops[stop['name']] for stop in matching_itinerary['stations'] + ] return True def __len__(self): @@ -964,11 +1184,22 @@ class Route: return iter(self.stops) def __repr__(self): - return ('Route(id={}, mode={}, ref={}, name={}, network={}, interval={}, ' - 'circular={}, num_stops={}, line_length={} m, from={}, to={}').format( - self.id, self.mode, self.ref, self.name, self.network, self.interval, - self.is_circular, len(self.stops), self.stops[-1].distance, - self.stops[0], self.stops[-1]) + return ( + 'Route(id={}, mode={}, ref={}, name={}, network={}, interval={}, ' + 'circular={}, num_stops={}, line_length={} m, from={}, to={}' + ).format( + self.id, + self.mode, + self.ref, + self.name, + self.network, + self.interval, + self.is_circular, + len(self.stops), + self.stops[-1].distance, + self.stops[0], + self.stops[-1], + ) class RouteMaster: @@ -979,17 +1210,25 @@ class RouteMaster: self.has_master = master is not None self.interval_from_master = False if master: - self.ref = master['tags'].get('ref', master['tags'].get('name', None)) + self.ref = master['tags'].get( + 'ref', master['tags'].get('name', None) + ) try: - self.colour = normalize_colour(master['tags'].get('colour', None)) + self.colour = normalize_colour( + master['tags'].get('colour', None) + ) except ValueError: self.colour = None try: - self.infill = normalize_colour(master['tags'].get('colour:infill', None)) + self.infill = normalize_colour( + master['tags'].get('colour:infill', None) + ) except ValueError: self.colour = None self.network = Route.get_network(master) - self.mode = master['tags'].get('route_master', None) # This tag is required, but okay + self.mode = master['tags'].get( + 'route_master', None + ) # This tag is required, but okay self.name = master['tags'].get('name', None) self.interval = Route.get_interval(master['tags']) self.interval_from_master = self.interval is not None @@ -1006,26 +1245,42 @@ class RouteMaster: if not self.network: self.network = route.network elif route.network and route.network != self.network: - city.error('Route has different network ("{}") from master "{}"'.format( - route.network, self.network), route.element) + city.error( + 'Route has different network ("{}") from master "{}"'.format( + route.network, self.network + ), + route.element, + ) if not self.colour: self.colour = route.colour elif route.colour and route.colour != self.colour: - city.warn('Route "{}" has different colour from master "{}"'.format( - route.colour, self.colour), route.element) + city.warn( + 'Route "{}" has different colour from master "{}"'.format( + route.colour, self.colour + ), + route.element, + ) if not self.infill: self.infill = route.infill elif route.infill and route.infill != self.infill: - city.warn('Route "{}" has different infill colour from master "{}"'.format( - route.infill, self.infill), route.element) + city.warn( + 'Route "{}" has different infill colour from master "{}"'.format( + route.infill, self.infill + ), + route.element, + ) if not self.ref: self.ref = route.ref elif route.ref != self.ref: - city.warn('Route "{}" has different ref from master "{}"'.format( - route.ref, self.ref), route.element) + city.warn( + 'Route "{}" has different ref from master "{}"'.format( + route.ref, self.ref + ), + route.element, + ) if not self.name: self.name = route.name @@ -1033,8 +1288,12 @@ class RouteMaster: if not self.mode: self.mode = route.mode elif route.mode != self.mode: - city.error('Incompatible PT mode: master has {} and route has {}'.format( - self.mode, route.mode), route.element) + city.error( + 'Incompatible PT mode: master has {} and route has {}'.format( + self.mode, route.mode + ), + route.element, + ) return if not self.interval_from_master and route.interval: @@ -1071,7 +1330,13 @@ class RouteMaster: def __repr__(self): return 'RouteMaster(id={}, mode={}, ref={}, name={}, network={}, num_variants={}'.format( - self.id, self.mode, self.ref, self.name, self.network, len(self.routes)) + self.id, + self.mode, + self.ref, + self.name, + self.network, + len(self.routes), + ) class City: @@ -1101,7 +1366,9 @@ class City: if not networks or len(networks[-1]) == 0: self.networks = [] else: - self.networks = set(filter(None, [x.strip() for x in networks[-1].split(';')])) + self.networks = set( + filter(None, [x.strip() for x in networks[-1].split(';')]) + ) if not networks or len(networks) < 2 or len(networks[0]) == 0: if self.overground: self.modes = DEFAULT_MODES_OVERGROUND @@ -1117,11 +1384,13 @@ class City: else: self.bbox = None - self.elements = {} # Dict el_id → el - self.stations = defaultdict(list) # Dict el_id → list of StopAreas - self.routes = {} # Dict route_ref → route - self.masters = {} # Dict el_id of route → route_master - self.stop_areas = defaultdict(list) # El_id → list of el_id of stop_area + self.elements = {} # Dict el_id → el + self.stations = defaultdict(list) # Dict el_id → list of StopAreas + self.routes = {} # Dict route_ref → route + self.masters = {} # Dict el_id of route → route_master + self.stop_areas = defaultdict( + list + ) # El_id → list of el_id of stop_area self.transfers = [] # List of lists of stop areas self.station_ids = set() # Set of stations' uid self.stops_and_platforms = set() # Set of stops and platforms el_id @@ -1131,8 +1400,10 @@ class City: if el: tags = el.get('tags', {}) message += ' ({} {}, "{}")'.format( - el['type'], el.get('id', el.get('ref')), - tags.get('name', tags.get('ref', ''))) + el['type'], + el.get('id', el.get('ref')), + tags.get('name', tags.get('ref', '')), + ) return message def warn(self, message, el=None): @@ -1152,8 +1423,10 @@ class City: def contains(self, el): center = el_center(el) if center: - return (self.bbox[0] <= center[1] <= self.bbox[2] and - self.bbox[1] <= center[0] <= self.bbox[3]) + return ( + self.bbox[0] <= center[1] <= self.bbox[2] + and self.bbox[1] <= center[0] <= self.bbox[3] + ) return False def add(self, el): @@ -1188,17 +1461,25 @@ class City: # the sag does - near the city bbox boundary continue if 'tags' not in el: - self.error('An untagged object {} in a stop_area_group'.format(k), sag) + self.error( + 'An untagged object {} in a stop_area_group'.format(k), sag + ) continue - if (el['type'] != 'relation' or - el['tags'].get('type') != 'public_transport' or - el['tags'].get('public_transport') != 'stop_area'): + if ( + el['type'] != 'relation' + or el['tags'].get('type') != 'public_transport' + or el['tags'].get('public_transport') != 'stop_area' + ): continue if k in self.stations: stoparea = self.stations[k][0] transfer.add(stoparea) if stoparea.transfer: - self.error('Stop area {} belongs to multiple interchanges'.format(k)) + self.error( + 'Stop area {} belongs to multiple interchanges'.format( + k + ) + ) stoparea.transfer = el_id(sag) if len(transfer) > 1: self.transfers.append(transfer) @@ -1208,10 +1489,17 @@ class City: processed_stop_areas = set() for el in self.elements.values(): if Station.is_station(el, self.modes): - # See PR https://github.com/mapsme/subways/pull/98 - if el['type'] == 'relation' and el['tags'].get('type') != 'multipolygon': - self.error("A railway station cannot be a relation of type '{}'".format( - el['tags'].get('type')), el) + # See PR https://github.com/mapsme/subways/pull/98 + if ( + el['type'] == 'relation' + and el['tags'].get('type') != 'multipolygon' + ): + self.error( + "A railway station cannot be a relation of type '{}'".format( + el['tags'].get('type') + ), + el, + ) continue st = Station(el, self) self.station_ids.add(st.id) @@ -1231,8 +1519,10 @@ class City: # Check that stops and platforms belong to single stop_area for sp in station.stops | station.platforms: if sp in self.stops_and_platforms: - self.warn('A stop or a platform {} belongs to multiple ' - 'stations, might be correct'.format(sp)) + self.warn( + 'A stop or a platform {} belongs to multiple ' + 'stations, might be correct'.format(sp) + ) else: self.stops_and_platforms.add(sp) @@ -1249,7 +1539,10 @@ class City: master_network = Route.get_network(master) else: master_network = None - if network not in self.networks and master_network not in self.networks: + if ( + network not in self.networks + and master_network not in self.networks + ): continue route = Route(el, self, master) @@ -1263,8 +1556,11 @@ class City: del self.routes[k] # And while we're iterating over relations, find interchanges - if (el['type'] == 'relation' and - el.get('tags', {}).get('public_transport', None) == 'stop_area_group'): + if ( + el['type'] == 'relation' + and el.get('tags', {}).get('public_transport', None) + == 'stop_area_group' + ): self.make_transfer(el) # Filter transfers, leaving only stations that belong to routes @@ -1293,30 +1589,36 @@ class City: 'stations_found': getattr(self, 'found_stations', 0), 'transfers_found': getattr(self, 'found_interchanges', 0), 'unused_entrances': getattr(self, 'unused_entrances', 0), - 'networks': getattr(self, 'found_networks', 0) + 'networks': getattr(self, 'found_networks', 0), } if not self.overground: - result.update({ - 'subwayl_expected': self.num_lines, - 'lightrl_expected': self.num_light_lines, - 'subwayl_found': getattr(self, 'found_lines', 0), - 'lightrl_found': getattr(self, 'found_light_lines', 0), - 'stations_expected': self.num_stations, - 'transfers_expected': self.num_interchanges, - }) + result.update( + { + 'subwayl_expected': self.num_lines, + 'lightrl_expected': self.num_light_lines, + 'subwayl_found': getattr(self, 'found_lines', 0), + 'lightrl_found': getattr(self, 'found_light_lines', 0), + 'stations_expected': self.num_stations, + 'transfers_expected': self.num_interchanges, + } + ) else: - result.update({ - 'stations_expected': 0, - 'transfers_expected': 0, - 'busl_expected': self.num_bus_lines, - 'trolleybusl_expected': self.num_trolleybus_lines, - 'traml_expected': self.num_tram_lines, - 'otherl_expected': self.num_other_lines, - 'busl_found': getattr(self, 'found_bus_lines', 0), - 'trolleybusl_found': getattr(self, 'found_trolleybus_lines', 0), - 'traml_found': getattr(self, 'found_tram_lines', 0), - 'otherl_found': getattr(self, 'found_other_lines', 0) - }) + result.update( + { + 'stations_expected': 0, + 'transfers_expected': 0, + 'busl_expected': self.num_bus_lines, + 'trolleybusl_expected': self.num_trolleybus_lines, + 'traml_expected': self.num_tram_lines, + 'otherl_expected': self.num_other_lines, + 'busl_found': getattr(self, 'found_bus_lines', 0), + 'trolleybusl_found': getattr( + self, 'found_trolleybus_lines', 0 + ), + 'traml_found': getattr(self, 'found_tram_lines', 0), + 'otherl_found': getattr(self, 'found_other_lines', 0), + } + ) result['warnings'] = self.warnings result['errors'] = self.errors return result @@ -1325,15 +1627,21 @@ class City: global used_entrances stop_areas = set() for el in self.elements.values(): - if (el['type'] == 'relation' and 'tags' in el and - el['tags'].get('public_transport') == 'stop_area' and - 'members' in el): + if ( + el['type'] == 'relation' + and 'tags' in el + and el['tags'].get('public_transport') == 'stop_area' + and 'members' in el + ): stop_areas.update([el_id(m) for m in el['members']]) unused = [] not_in_sa = [] for el in self.elements.values(): - if (el['type'] == 'node' and 'tags' in el and - el['tags'].get('railway') == 'subway_entrance'): + if ( + el['type'] == 'node' + and 'tags' in el + and el['tags'].get('railway') == 'subway_entrance' + ): i = el_id(el) if i in self.stations: used_entrances.add(i) @@ -1344,11 +1652,17 @@ class City: self.unused_entrances = len(unused) self.entrances_not_in_stop_areas = len(not_in_sa) if unused: - self.warn('Found {} entrances not used in routes or stop_areas: {}'.format( - len(unused), format_elid_list(unused))) + self.warn( + 'Found {} entrances not used in routes or stop_areas: {}'.format( + len(unused), format_elid_list(unused) + ) + ) if not_in_sa: - self.warn('{} subway entrances are not in stop_area relations: {}'.format( - len(not_in_sa), format_elid_list(not_in_sa))) + self.warn( + '{} subway entrances are not in stop_area relations: {}'.format( + len(not_in_sa), format_elid_list(not_in_sa) + ) + ) def check_return_routes(self, rmaster): variants = {} @@ -1362,8 +1676,10 @@ class City: if variant[0].stoparea.transfer == variant[-1].stoparea.transfer: t = (variant[0].stoparea.id, variant[-1].stoparea.id) else: - t = (variant[0].stoparea.transfer or variant[0].stoparea.id, - variant[-1].stoparea.transfer or variant[-1].stoparea.id) + t = ( + variant[0].stoparea.transfer or variant[0].stoparea.id, + variant[-1].stoparea.transfer or variant[-1].stoparea.id, + ) if t in variants: continue variants[t] = variant.element @@ -1373,36 +1689,64 @@ class City: have_return.add(tr) if len(variants) == 0: - self.error('An empty route master {}. Please set construction:route ' - 'if it is under construction'.format(rmaster.id)) + self.error( + 'An empty route master {}. Please set construction:route ' + 'if it is under construction'.format(rmaster.id) + ) elif len(variants) == 1: - self.error_if(not rmaster.best.is_circular, 'Only one route in route_master. ' - 'Please check if it needs a return route', rmaster.best.element) + self.error_if( + not rmaster.best.is_circular, + 'Only one route in route_master. ' + 'Please check if it needs a return route', + rmaster.best.element, + ) else: for t, rel in variants.items(): if t not in have_return: self.warn('Route does not have a return direction', rel) def validate_lines(self): - self.found_light_lines = len([x for x in self.routes.values() if x.mode != 'subway']) + self.found_light_lines = len( + [x for x in self.routes.values() if x.mode != 'subway'] + ) self.found_lines = len(self.routes) - self.found_light_lines if self.found_lines != self.num_lines: - self.error('Found {} subway lines, expected {}'.format( - self.found_lines, self.num_lines)) + self.error( + 'Found {} subway lines, expected {}'.format( + self.found_lines, self.num_lines + ) + ) if self.found_light_lines != self.num_light_lines: - self.error('Found {} light rail lines, expected {}'.format( - self.found_light_lines, self.num_light_lines)) + self.error( + 'Found {} light rail lines, expected {}'.format( + self.found_light_lines, self.num_light_lines + ) + ) def validate_overground_lines(self): - self.found_tram_lines = len([x for x in self.routes.values() if x.mode == 'tram']) - self.found_bus_lines = len([x for x in self.routes.values() if x.mode == 'bus']) - self.found_trolleybus_lines = len([x for x in self.routes.values() - if x.mode == 'trolleybus']) - self.found_other_lines = len([x for x in self.routes.values() - if x.mode not in ('bus', 'trolleybus', 'tram')]) + self.found_tram_lines = len( + [x for x in self.routes.values() if x.mode == 'tram'] + ) + self.found_bus_lines = len( + [x for x in self.routes.values() if x.mode == 'bus'] + ) + self.found_trolleybus_lines = len( + [x for x in self.routes.values() if x.mode == 'trolleybus'] + ) + self.found_other_lines = len( + [ + x + for x in self.routes.values() + if x.mode not in ('bus', 'trolleybus', 'tram') + ] + ) if self.found_tram_lines != self.num_tram_lines: - self.error_if(self.found_tram_lines == 0, 'Found {} tram lines, expected {}'.format( - self.found_tram_lines, self.num_tram_lines)) + self.error_if( + self.found_tram_lines == 0, + 'Found {} tram lines, expected {}'.format( + self.found_tram_lines, self.num_tram_lines + ), + ) def validate(self): networks = Counter() @@ -1419,8 +1763,11 @@ class City: self.found_stations += len(route_stations) if unused_stations: self.unused_stations = len(unused_stations) - self.warn('{} unused stations: {}'.format( - self.unused_stations, format_elid_list(unused_stations))) + self.warn( + '{} unused stations: {}'.format( + self.unused_stations, format_elid_list(unused_stations) + ) + ) self.count_unused_entrances() self.found_interchanges = len(self.transfers) @@ -1431,21 +1778,37 @@ class City: if self.found_stations != self.num_stations: msg = 'Found {} stations in routes, expected {}'.format( - self.found_stations, self.num_stations) - self.error_if(not (0 <= - (self.num_stations - self.found_stations) / self.num_stations <= - ALLOWED_STATIONS_MISMATCH), msg) + self.found_stations, self.num_stations + ) + self.error_if( + not ( + 0 + <= (self.num_stations - self.found_stations) + / self.num_stations + <= ALLOWED_STATIONS_MISMATCH + ), + msg, + ) if self.found_interchanges != self.num_interchanges: msg = 'Found {} interchanges, expected {}'.format( - self.found_interchanges, self.num_interchanges) - self.error_if(self.num_interchanges != 0 and not - ((self.num_interchanges - self.found_interchanges) / - self.num_interchanges <= ALLOWED_TRANSFERS_MISMATCH), msg) + self.found_interchanges, self.num_interchanges + ) + self.error_if( + self.num_interchanges != 0 + and not ( + (self.num_interchanges - self.found_interchanges) + / self.num_interchanges + <= ALLOWED_TRANSFERS_MISMATCH + ), + msg, + ) self.found_networks = len(networks) if len(networks) > max(1, len(self.networks)): - n_str = '; '.join(['{} ({})'.format(k, v) for k, v in networks.items()]) + n_str = '; '.join( + ['{} ({})'.format(k, v) for k, v in networks.items()] + ) self.warn('More than one network: {}'.format(n_str)) @@ -1453,8 +1816,11 @@ def find_transfers(elements, cities): transfers = [] stop_area_groups = [] for el in elements: - if (el['type'] == 'relation' and 'members' in el and - el.get('tags', {}).get('public_transport') == 'stop_area_group'): + if ( + el['type'] == 'relation' + and 'members' in el + and el.get('tags', {}).get('public_transport') == 'stop_area_group' + ): stop_area_groups.append(el) # StopArea.id uniquely identifies a StopArea. @@ -1473,7 +1839,9 @@ def find_transfers(elements, cities): k = el_id(m) if k not in stop_area_ids: continue - transfer.update(stop_area_objects[sa_id] for sa_id in stop_area_ids[k]) + transfer.update( + stop_area_objects[sa_id] for sa_id in stop_area_ids[k] + ) if len(transfer) > 1: transfers.append(transfer) return transfers @@ -1483,22 +1851,41 @@ def get_unused_entrances_geojson(elements): global used_entrances features = [] for el in elements: - if (el['type'] == 'node' and 'tags' in el and - el['tags'].get('railway') == 'subway_entrance'): + if ( + el['type'] == 'node' + and 'tags' in el + and el['tags'].get('railway') == 'subway_entrance' + ): if el_id(el) not in used_entrances: geometry = {'type': 'Point', 'coordinates': el_center(el)} - properties = {k: v for k, v in el['tags'].items() - if k not in ('railway', 'entrance')} - features.append({'type': 'Feature', 'geometry': geometry, 'properties': properties}) + properties = { + k: v + for k, v in el['tags'].items() + if k not in ('railway', 'entrance') + } + features.append( + { + 'type': 'Feature', + 'geometry': geometry, + 'properties': properties, + } + ) return {'type': 'FeatureCollection', 'features': features} def download_cities(overground=False): - url = 'https://docs.google.com/spreadsheets/d/{}/export?format=csv{}'.format( - SPREADSHEET_ID, '&gid=1881416409' if overground else '') + url = ( + 'https://docs.google.com/spreadsheets/d/{}/export?format=csv{}'.format( + SPREADSHEET_ID, '&gid=1881416409' if overground else '' + ) + ) response = urllib.request.urlopen(url) if response.getcode() != 200: - raise Exception('Failed to download cities spreadsheet: HTTP {}'.format(response.getcode())) + raise Exception( + 'Failed to download cities spreadsheet: HTTP {}'.format( + response.getcode() + ) + ) data = response.read().decode('utf-8') r = csv.reader(data.splitlines()) next(r) # skipping the header @@ -1508,6 +1895,8 @@ def download_cities(overground=False): if len(row) > 8 and row[8]: cities.append(City(row, overground)) if row[0].strip() in names: - logging.warning('Duplicate city name in the google spreadsheet: %s', row[0]) + logging.warning( + 'Duplicate city name in the google spreadsheet: %s', row[0] + ) names.add(row[0].strip()) return cities diff --git a/validation_to_html.py b/validation_to_html.py index 75fab0a..7790fd4 100755 --- a/validation_to_html.py +++ b/validation_to_html.py @@ -17,7 +17,7 @@ class CityData: 'good_cities': 0, 'total_cities': 1 if city else 0, 'num_errors': 0, - 'num_warnings': 0 + 'num_warnings': 0, } self.slug = None if city: @@ -51,18 +51,34 @@ class CityData: return '1' if v1 == v2 else '0' for k in self.data: - s = s.replace('{'+k+'}', str(self.data[k])) + s = s.replace('{' + k + '}', str(self.data[k])) s = s.replace('{slug}', self.slug or '') - for k in ('subwayl', 'lightrl', 'stations', 'transfers', 'busl', - 'trolleybusl', 'traml', 'otherl'): - if k+'_expected' in self.data: - s = s.replace('{='+k+'}', - test_eq(self.data[k+'_found'], self.data[k+'_expected'])) - s = s.replace('{=cities}', - test_eq(self.data['good_cities'], self.data['total_cities'])) - s = s.replace('{=entrances}', test_eq(self.data['unused_entrances'], 0)) + for k in ( + 'subwayl', + 'lightrl', + 'stations', + 'transfers', + 'busl', + 'trolleybusl', + 'traml', + 'otherl', + ): + if k + '_expected' in self.data: + s = s.replace( + '{=' + k + '}', + test_eq( + self.data[k + '_found'], self.data[k + '_expected'] + ), + ) + s = s.replace( + '{=cities}', + test_eq(self.data['good_cities'], self.data['total_cities']), + ) + s = s.replace( + '{=entrances}', test_eq(self.data['unused_entrances'], 0) + ) for k in ('errors', 'warnings'): - s = s.replace('{='+k+'}', test_eq(self.data['num_'+k], 0)) + s = s.replace('{=' + k + '}', test_eq(self.data['num_' + k], 0)) return s @@ -72,10 +88,19 @@ def tmpl(s, data=None, **kwargs): if kwargs: for k, v in kwargs.items(): if v is not None: - s = s.replace('{'+k+'}', str(v)) - s = re.sub(r'\{\?'+k+r'\}(.+?)\{end\}', r'\1' if v else '', s, flags=re.DOTALL) + s = s.replace('{' + k + '}', str(v)) + s = re.sub( + r'\{\?' + k + r'\}(.+?)\{end\}', + r'\1' if v else '', + s, + flags=re.DOTALL, + ) s = s.replace('{date}', date) - google_url = 'https://docs.google.com/spreadsheets/d/{}/edit?usp=sharing'.format(SPREADSHEET_ID) + google_url = ( + 'https://docs.google.com/spreadsheets/d/{}/edit?usp=sharing'.format( + SPREADSHEET_ID + ) + ) s = s.replace('{google}', google_url) return s @@ -88,13 +113,18 @@ RE_COORDS = re.compile(r'\((-?\d+\.\d+), (-?\d+\.\d+)\)') def osm_links(s): """Converts object mentions to HTML links.""" + def link(m): return '{}'.format( - EXPAND_OSM_TYPE[m.group(1)[0]], m.group(2), m.group(0)) + EXPAND_OSM_TYPE[m.group(1)[0]], m.group(2), m.group(0) + ) + s = RE_SHORT.sub(link, s) s = RE_FULL.sub(link, s) s = RE_COORDS.sub( - r'(pos)', s) + r'(pos)', + s, + ) return s @@ -104,7 +134,9 @@ def esc(s): if len(sys.argv) < 2: print('Reads a log from subway validator and prepares HTML files.') - print('Usage: {} []'.format(sys.argv[0])) + print( + 'Usage: {} []'.format(sys.argv[0]) + ) sys.exit(1) with open(sys.argv[1], 'r', encoding='utf-8') as f: @@ -131,27 +163,67 @@ for continent in sorted(continents.keys()): content = '' for country in sorted(c_by_c[continent]): country_file_name = country.lower().replace(' ', '-') + '.html' - content += tmpl(INDEX_COUNTRY, countries[country], file=country_file_name, - country=country, continent=continent) - country_file = open(os.path.join(path, country_file_name), 'w', encoding='utf-8') - country_file.write(tmpl(COUNTRY_HEADER, country=country, continent=continent, - overground=overground, subways=not overground)) + content += tmpl( + INDEX_COUNTRY, + countries[country], + file=country_file_name, + country=country, + continent=continent, + ) + country_file = open( + os.path.join(path, country_file_name), 'w', encoding='utf-8' + ) + country_file.write( + tmpl( + COUNTRY_HEADER, + country=country, + continent=continent, + overground=overground, + subways=not overground, + ) + ) for name, city in sorted(data.items()): if city.country == country: file_base = os.path.join(path, city.slug) - yaml_file = city.slug + '.yaml' if os.path.exists(file_base + '.yaml') else None - json_file = city.slug + '.geojson' if os.path.exists( - file_base + '.geojson') else None + yaml_file = ( + city.slug + '.yaml' + if os.path.exists(file_base + '.yaml') + else None + ) + json_file = ( + city.slug + '.geojson' + if os.path.exists(file_base + '.geojson') + else None + ) e = '
'.join([osm_links(esc(e)) for e in city.errors]) w = '
'.join([osm_links(esc(w)) for w in city.warnings]) - country_file.write(tmpl(COUNTRY_CITY, city, - city=name, country=country, continent=continent, - yaml=yaml_file, json=json_file, subways=not overground, - errors=e, warnings=w, overground=overground)) - country_file.write(tmpl(COUNTRY_FOOTER, country=country, continent=continent)) + country_file.write( + tmpl( + COUNTRY_CITY, + city, + city=name, + country=country, + continent=continent, + yaml=yaml_file, + json=json_file, + subways=not overground, + errors=e, + warnings=w, + overground=overground, + ) + ) + country_file.write( + tmpl(COUNTRY_FOOTER, country=country, continent=continent) + ) country_file.close() - index.write(tmpl(INDEX_CONTINENT, continents[continent], - content=content, continent=continent)) + index.write( + tmpl( + INDEX_CONTINENT, + continents[continent], + content=content, + continent=continent, + ) + ) index.write(tmpl(INDEX_FOOTER)) index.close()