From a5b13e9ea934207d614d69d6eafeeb6a2424b591 Mon Sep 17 00:00:00 2001 From: Ilya Zverev Date: Tue, 17 Oct 2017 20:02:57 +0300 Subject: [PATCH] Data from route_master, geojson for unused subway entrances --- .gitignore | 1 + mapsme_subways.py | 15 ++++++-- subway_structure.py | 84 ++++++++++++++++++++++++++++++++++++--------- 3 files changed, 80 insertions(+), 20 deletions(-) diff --git a/.gitignore b/.gitignore index 908478d..6ab694a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ __pycache__/ *.log *.json +*.geojson *.osm validate.sh diff --git a/mapsme_subways.py b/mapsme_subways.py index a541d44..ab62f20 100755 --- a/mapsme_subways.py +++ b/mapsme_subways.py @@ -7,7 +7,12 @@ import sys import time import urllib.parse import urllib.request -from subway_structure import download_cities, find_transfers + +from subway_structure import ( + download_cities, + find_transfers, + get_unused_entrances_geojson, +) def overpass_request(bboxes=None): @@ -80,7 +85,6 @@ def load_xml(f): el['members'] = members elements.append(el) element.clear() - logging.info('Read %s elements, now finding centers of ways and relations', len(elements)) # Now make centers, assuming relations go after ways ways = {} @@ -154,6 +158,8 @@ if __name__ == '__main__': help='Use city boundaries to query Overpass API instead of querying the world') 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') + parser.add_argument('-e', '--entrances', type=argparse.FileType('w'), + help='Export unused subway entrances as GeoJSON here') parser.add_argument('-l', '--log', type=argparse.FileType('w'), help='Validation JSON file name') parser.add_argument('-o', '--output', help='JSON file for MAPS.ME') @@ -222,7 +228,10 @@ if __name__ == '__main__': res = [x.get_validation_result() for x in cities] json.dump(res, options.log) - # Finally, preparing a JSON file for MAPS.ME + if options.entrances: + json.dump(get_unused_entrances_geojson(osm), options.entrances) + + # Finally, prepare a JSON file for MAPS.ME if options.output: networks = [c.for_mapsme() for c in good_cities] with open(options.output, 'w') as f: diff --git a/subway_structure.py b/subway_structure.py index c767c52..64b8b45 100644 --- a/subway_structure.py +++ b/subway_structure.py @@ -11,6 +11,7 @@ MAX_DISTANCE_NEARBY = 150 # in meters MODES = ('subway', 'light_rail', 'monorail') transfers = [] +used_entrances = set() def el_id(el): @@ -88,6 +89,8 @@ class Station: for m in self.stop_area['members']: k = el_id(m) if k in city.elements: + if Station.is_station(city.elements[k]) and k != self.id: + city.error('Stop area has two stations', self.stop_area) self.elements.add(k) else: # Otherwise add nearby entrances and stop positions @@ -210,26 +213,48 @@ class Route: class RouteMaster: - def __init__(self, route): - self.routes = [route] - self.best = route - self.ref = route.ref - self.network = route.network - self.mode = route.mode + def __init__(self, master=None): + self.routes = [] + self.best = None + if master: + self.ref = master['tags'].get('ref', master['tags'].get('name', None)) + self.colour = master['tags'].get('colour', None) + self.network = Route.get_network(master) + self.mode = master['tags'].get('route_master', None) # This tag is required, but okay + else: + self.ref = None + self.colour = None + self.network = None + self.mode = None def add(self, route, city): - if route.network != self.network: + if not self.network: + self.network = route.network + elif route.network != self.network: city.error('Route has different network ("{}") from master "{}"'.format( route.network, self.network), route.element) - if route.ref != self.ref: + + if not self.colour: + self.colour = route.colour + elif route.colour != self.colour: + city.warn('Route "{}" has different colour from master "{}"'.format( + route.colour, self.colour), 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) - if route.mode != self.mode: + + 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) return + self.routes.append(route) - if len(route.stops) > len(self.best.stops): + if not self.best or len(route.stops) > len(self.best.stops): self.best = route def __len__(self): @@ -347,33 +372,41 @@ class City: s_id = el_id(el) if s_id in self.stop_areas: stations = [] - # TODO: Check that each stop_area contains only one station for sa in self.stop_areas[s_id]: stations.append(Station(el, self, sa)) else: stations = [Station(el, self)] + for station in stations: self.station_ids.add(station.id) - for e in station.elements: + for st_el in station.elements: # TODO: Check for duplicates for platforms and stops? - self.stations[e].append(station) + self.stations[st_el].append(station) for el in self.elements.values(): if Route.is_route(el): if self.networks and Route.get_network(el) not in self.networks: continue route = Route(el, self) - k = self.masters.get(route.id, route.ref) - if k not in self.routes: - self.routes[k] = RouteMaster(route) + if route.id in self.masters: + k = self.masters[route.id] + master = self.elements.get(k, None) else: - self.routes[k].add(route, self) + k = route.ref + master = None + if k not in self.routes: + self.routes[k] = RouteMaster(master) + self.routes[k].add(route, self) + # Sometimes adding a route to a newly initialized RouteMaster can fail + if len(self.routes[k]) == 0: + del self.routes[k] if (el['type'] == 'relation' and el.get('tags', {}).get('public_transport', None) == 'stop_area_group'): self.make_transfer(el) def count_unused_entrances(self): + global used_entrances stop_areas = set() for el in self.elements.values(): if (el['type'] == 'relation' and 'tags' in el and @@ -388,6 +421,8 @@ class City: i = el_id(el) if i not in self.stations: unused.append(i) + else: + used_entrances.add(i) if i not in stop_areas: not_in_sa.append(i) self.unused_entrances = len(unused) @@ -441,6 +476,8 @@ class City: def find_transfers(elements, cities): + global transfers + transfers = [] stop_area_groups = [] for el in elements: if (el['type'] == 'relation' and 'members' in el and @@ -465,6 +502,19 @@ def find_transfers(elements, cities): return transfers +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_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 != 'railway'} + features.append({'type': 'Feature', 'geometry': geometry, 'properties': properties}) + return {'type': 'FeatureCollection', 'features': features} + + def download_cities(): url = 'https://docs.google.com/spreadsheets/d/{}/export?format=csv'.format(SPREADSHEET_ID) response = urllib.request.urlopen(url)