Merge pull request #5 from organicmaps/upstream

Upstream fixes
This commit is contained in:
Alexander Borsuk 2023-05-27 15:39:16 +02:00 committed by GitHub
commit cf8f3cdf91
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 547 additions and 111 deletions

View file

@ -19,10 +19,10 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.8
- name: Set up Python 3.11
uses: actions/setup-python@v3
with:
python-version: "3.8"
python-version: "3.11"
- name: Install dependencies
run: |
python -m pip install --upgrade pip

View file

@ -53,7 +53,7 @@ a city's bbox has been extended.
A single city or a country with few metro networks can be validated much faster
if you allow the `process_subway.py` to fetch data from Overpass API. Here are the steps:
1. Python3 interpreter required (3.8+)
1. Python3 interpreter required (3.11+)
2. Clone the repo
```
git clone https://github.com/alexey-zakharenkov/subways.git subways_validator

View file

@ -11,7 +11,6 @@ import time
import urllib.parse
import urllib.request
from functools import partial
from typing import Dict, List, Optional, Tuple
import processors
from subway_io import (
@ -30,17 +29,18 @@ from subway_structure import (
MODES_RAPID,
)
DEFAULT_SPREADSHEET_ID = "1SEW1-NiNOnA2qDwievcxYV1FOaQl1mb1fdeyqAxHu3k"
DEFAULT_CITIES_INFO_URL = (
"https://docs.google.com/spreadsheets/d/"
f"{DEFAULT_SPREADSHEET_ID}/export?format=csv"
)
Point = Tuple[float, float]
Point = tuple[float, float]
def overpass_request(overground, overpass_api, bboxes):
def overpass_request(
overground: bool, overpass_api: str, bboxes: list[list[float]]
) -> list[dict]:
query = "[out:json][timeout:1000];("
modes = MODES_OVERGROUND if overground else MODES_RAPID
for bbox in bboxes:
@ -65,7 +65,9 @@ def overpass_request(overground, overpass_api, bboxes):
return json.load(response)["elements"]
def multi_overpass(overground, overpass_api, bboxes):
def multi_overpass(
overground: bool, overpass_api: str, bboxes: list[list[float]]
) -> list[dict]:
SLICE_SIZE = 10
INTERREQUEST_WAIT = 5 # in seconds
result = []
@ -77,13 +79,13 @@ def multi_overpass(overground, overpass_api, bboxes):
return result
def slugify(name):
def slugify(name: str) -> str:
return re.sub(r"[^a-z0-9_-]+", "", name.lower().replace(" ", "_"))
def get_way_center(
element: dict, node_centers: Dict[int, Point]
) -> Optional[Point]:
element: dict, node_centers: dict[int, Point]
) -> Point | None:
"""
:param element: dict describing OSM element
:param node_centers: osm_id => (lat, lon)
@ -123,11 +125,11 @@ def get_way_center(
def get_relation_center(
element: dict,
node_centers: Dict[int, Point],
way_centers: Dict[int, Point],
relation_centers: Dict[int, Point],
node_centers: dict[int, Point],
way_centers: dict[int, Point],
relation_centers: dict[int, Point],
ignore_unlocalized_child_relations: bool = False,
) -> Optional[Point]:
) -> Point | None:
"""
:param element: dict describing OSM element
:param node_centers: osm_id => (lat, lon)
@ -176,14 +178,14 @@ def get_relation_center(
return element["center"]["lat"], element["center"]["lon"]
def calculate_centers(elements):
def calculate_centers(elements: list[dict]) -> None:
"""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.
"""
nodes: Dict[int, Point] = {} # id => (lat, lon)
ways: Dict[int, Point] = {} # id => (lat, lon)
relations: Dict[int, Point] = {} # id => (lat, lon)
nodes: dict[int, Point] = {} # id => (lat, lon)
ways: dict[int, Point] = {} # id => (lat, lon)
relations: dict[int, Point] = {} # id => (lat, lon)
unlocalized_relations = [] # 'unlocalized' means the center of the
# relation has not been calculated yet
@ -202,7 +204,7 @@ def calculate_centers(elements):
def iterate_relation_centers_calculation(
ignore_unlocalized_child_relations: bool,
) -> List[int]:
) -> list[dict]:
unlocalized_relations_upd = []
for rel in unlocalized_relations:
if center := get_relation_center(
@ -229,14 +231,16 @@ def calculate_centers(elements):
unlocalized_relations = unlocalized_relations_upd
def add_osm_elements_to_cities(osm_elements, cities):
def add_osm_elements_to_cities(
osm_elements: list[dict], cities: list[City]
) -> None:
for el in osm_elements:
for c in cities:
if c.contains(el):
c.add(el)
def validate_cities(cities):
def validate_cities(cities: list[City]) -> list[City]:
"""Validate cities. Return list of good cities."""
good_cities = []
for c in cities:
@ -266,7 +270,7 @@ def validate_cities(cities):
def get_cities_info(
cities_info_url: str = DEFAULT_CITIES_INFO_URL,
) -> List[dict]:
) -> list[dict]:
response = urllib.request.urlopen(cities_info_url)
if (
not cities_info_url.startswith("file://")
@ -310,14 +314,14 @@ def get_cities_info(
def prepare_cities(
cities_info_url: str = DEFAULT_CITIES_INFO_URL, overground: bool = False
) -> List[City]:
) -> list[City]:
if overground:
raise NotImplementedError("Overground transit not implemented yet")
cities_info = get_cities_info(cities_info_url)
return list(map(partial(City, overground=overground), cities_info))
def main():
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument(
"--cities-info-url",

View file

@ -1,6 +1,7 @@
import math
import re
from collections import Counter, defaultdict
from itertools import islice
from css_colours import normalize_colour
@ -1161,18 +1162,21 @@ class Route:
tracks = self.get_truncated_tracks(tracks)
return tracks
def check_stops_order_by_angle(self):
def check_stops_order_by_angle(self) -> tuple[list, list]:
disorder_warnings = []
disorder_errors = []
for si in range(len(self.stops) - 2):
for i, route_stop in enumerate(
islice(self.stops, 1, len(self.stops) - 1), start=1
):
angle = angle_between(
self.stops[si].stop,
self.stops[si + 1].stop,
self.stops[si + 2].stop,
self.stops[i - 1].stop,
route_stop.stop,
self.stops[i + 1].stop,
)
if angle < ALLOWED_ANGLE_BETWEEN_STOPS:
msg = (
f'Angle between stops around "{self.stops[si + 1]}" '
"Angle between stops around "
f'"{route_stop.stoparea.name}" {route_stop.stop} '
f"is too narrow, {angle} degrees"
)
if angle < DISALLOWED_ANGLE_BETWEEN_STOPS:
@ -1181,7 +1185,7 @@ class Route:
disorder_warnings.append(msg)
return disorder_warnings, disorder_errors
def check_stops_order_on_tracks_direct(self, stop_sequence):
def check_stops_order_on_tracks_direct(self, stop_sequence) -> str | None:
"""Checks stops order on tracks, following stop_sequence
in direct order only.
:param stop_sequence: list of dict{'route_stop', 'positions_on_rails',
@ -1206,12 +1210,13 @@ class Route:
allowed_order_violations -= 1
else:
route_stop = stop_data["route_stop"]
return 'Stops on tracks are unordered near "{}" {}'.format(
route_stop.stoparea.name, route_stop.stop
return (
"Stops on tracks are unordered near "
f'"{route_stop.stoparea.name}" {route_stop.stop}'
)
max_position_on_rails = positions_on_rails[suitable_occurrence]
def check_stops_order_on_tracks(self, projected_stops_data):
def check_stops_order_on_tracks(self, projected_stops_data) -> str | None:
"""Checks stops order on tracks, trying direct and reversed
order of stops in the stop_sequence.
:param projected_stops_data: info about RouteStops that belong to the
@ -1517,25 +1522,23 @@ class City:
self.errors = []
self.warnings = []
self.notices = []
self.id = int(city_data["id"])
self.try_fill_int_attribute(city_data, "id")
self.name = city_data["name"]
self.country = city_data["country"]
self.continent = city_data["continent"]
self.overground = overground
if not overground:
self.num_stations = int(city_data["num_stations"])
self.num_lines = int(city_data["num_lines"] or "0")
self.num_light_lines = int(city_data["num_light_lines"] or "0")
self.num_interchanges = int(city_data["num_interchanges"] or "0")
self.try_fill_int_attribute(city_data, "num_stations")
self.try_fill_int_attribute(city_data, "num_lines", "0")
self.try_fill_int_attribute(city_data, "num_light_lines", "0")
self.try_fill_int_attribute(city_data, "num_interchanges", "0")
else:
self.num_tram_lines = int(city_data["num_tram_lines"] or "0")
self.num_trolleybus_lines = int(
city_data["num_trolleybus_lines"] or "0"
)
self.num_bus_lines = int(city_data["num_bus_lines"] or "0")
self.num_other_lines = int(city_data["num_other_lines"] or "0")
self.try_fill_int_attribute(city_data, "num_tram_lines", "0")
self.try_fill_int_attribute(city_data, "num_trolleybus_lines", "0")
self.try_fill_int_attribute(city_data, "num_bus_lines", "0")
self.try_fill_int_attribute(city_data, "num_other_lines", "0")
# Aquiring list of networks and modes
# Acquiring list of networks and modes
networks = (
None
if not city_data["networks"]
@ -1574,6 +1577,33 @@ class City:
self.stops_and_platforms = set() # Set of stops and platforms el_id
self.recovery_data = None
def try_fill_int_attribute(
self, city_data: dict, attr: str, default: str | None = None
) -> None:
"""Try to convert string value to int. Conversion is considered
to fail if one of the following is true:
* attr is not empty and data type casting fails;
* attr is empty and no default value is given.
In such cases the city is marked as bad by adding an error
to the city validation log.
"""
attr_value = city_data[attr]
if not attr_value and default is not None:
attr_value = default
try:
attr_int = int(attr_value)
except ValueError:
print_value = (
f"{city_data[attr]}" if city_data[attr] else "<empty>"
)
self.error(
f"Configuration error: wrong value for {attr}: {print_value}"
)
setattr(self, attr, 0)
else:
setattr(self, attr, attr_int)
@staticmethod
def log_message(message, el):
if el:
@ -1814,12 +1844,12 @@ class City:
if not self.overground:
result.update(
{
"subwayl_expected": self.num_lines,
"lightrl_expected": self.num_light_lines,
"subwayl_expected": getattr(self, "num_lines", 0),
"lightrl_expected": getattr(self, "num_light_lines", 0),
"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,
"stations_expected": getattr(self, "num_stations", 0),
"transfers_expected": getattr(self, "num_interchanges", 0),
}
)
else:
@ -1827,10 +1857,12 @@ class City:
{
"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_expected": getattr(self, "num_bus_lines", 0),
"trolleybusl_expected": getattr(
self, "num_trolleybus_lines", 0
),
"traml_expected": getattr(self, "num_tram_lines", 0),
"otherl_expected": getattr(self, "num_other_lines", 0),
"busl_found": getattr(self, "found_bus_lines", 0),
"trolleybusl_found": getattr(
self, "found_trolleybus_lines", 0
@ -2006,7 +2038,8 @@ class City:
)
log_function = (
self.error
if not (
if self.num_stations > 0
and not (
0
<= (self.num_stations - self.found_stations)
/ self.num_stations

View file

@ -0,0 +1,12 @@
#,City,Country,Region,Stations,Subway Lines,Light Rail +Monorail,Interchanges,"BBox (lon, lat)",Networks (opt.),Comment,Source,
291,Moscow,Russia,Europe,335,14,3,66,,"subway,train:Московский метрополитен;МЦК;МЦД","No bbox - skip the city",
,Moscow - Aeroexpress,Russia,Europe,22,0,3,0,"37.170,55.385,38.028,56.022",train:Аэроэкспресс,"No id - skip the city",https://aeroexpress.ru
292,Nizhny Novgorod,Russia,Europe,16,2,0,,"43.759918,56.1662,44.13208,56.410862",,"No configuration errors",,,
NBS,Novosibirsk,Russia,Europe,13,2,0,1,"82.774773,54.926747,83.059044,55.127864",,"Non-numeric ID",,,
294,Saint Petersburg,Russia,Europe,72,,,,"30.0648,59.7509,30.5976,60.1292",,"Empty line counts - no problem at CSV parsing stage",,
,,,,,,,,,,,,
295,Samara,Russia,Europe,10x,1,0,0,"50.011826,53.094024,50.411453,53.384147",,"Non-numeric station count",,
296,Volgograd,Russia,Europe,40,zero,2zero,0,"44.366704,48.636024,44.62302,48.81765",,"Non-numbers in subway and light_rail line counts",,
297,Yekaterinburg,Russia,Europe,,1,0,0,"60.460854,56.730505,60.727272,56.920997",,"Empty station count",,
,,,,,,,,,,,,
Can't render this file because it has a wrong number of fields in line 2.

View file

@ -37,7 +37,7 @@ sample_networks = {
</relation>
</osm>
""",
"station_count": 2,
"num_stations": 2,
"tracks": [],
"extended_tracks": [
(0.0, 0.0),
@ -100,7 +100,7 @@ sample_networks = {
</relation>
</osm>
""",
"station_count": 2,
"num_stations": 2,
"tracks": [
(0.0, 0.0),
(1.0, 0.0),
@ -190,7 +190,7 @@ sample_networks = {
</relation>
</osm>
""",
"station_count": 6,
"num_stations": 6,
"tracks": [],
"extended_tracks": [
(0.0, 0.0),
@ -289,7 +289,7 @@ sample_networks = {
</relation>
</osm>
""",
"station_count": 6,
"num_stations": 6,
"tracks": [
(0.0, 0.0),
(1.0, 0.0),
@ -401,7 +401,7 @@ sample_networks = {
</relation>
</osm>
""",
"station_count": 6,
"num_stations": 6,
"tracks": [
(0.0, 0.0),
(1.0, 0.0),
@ -511,7 +511,7 @@ sample_networks = {
</relation>
</osm>
""",
"station_count": 6,
"num_stations": 6,
"tracks": [
(1.0, 0.0),
(2.0, 0.0),
@ -620,7 +620,7 @@ sample_networks = {
</relation>
</osm>
""",
"station_count": 6,
"num_stations": 6,
"tracks": [
(1.0, 0.0),
(2.0, 0.0),
@ -725,7 +725,7 @@ sample_networks = {
</relation>
</osm>
""",
"station_count": 6,
"num_stations": 6,
"tracks": [
(0.0, 0.0),
(1.0, 0.0),
@ -826,7 +826,7 @@ sample_networks = {
</relation>
</osm>
""",
"station_count": 6,
"num_stations": 6,
"tracks": [
(4.0, 0.0),
(5.0, 0.0),
@ -937,7 +937,7 @@ sample_networks = {
</relation>
</osm>
""",
"station_count": 6,
"num_stations": 6,
"tracks": [
(-1.0, 0.0),
(0.0, 0.0),
@ -1069,7 +1069,7 @@ sample_networks = {
</relation>
</osm>
""",
"station_count": 6,
"num_stations": 6,
"tracks": [
(-1.0, 0.0),
(0.0, 0.0),
@ -1189,7 +1189,7 @@ sample_networks = {
</relation>
</osm>
""",
"station_count": 6,
"num_stations": 6,
"tracks": [
(-1.0, 0.0),
(6.0, 0.0),
@ -1304,7 +1304,7 @@ sample_networks = {
</relation>
</osm>
""",
"station_count": 6,
"num_stations": 6,
"tracks": [
(1.0, 0.0),
(4.0, 0.0),
@ -1377,7 +1377,7 @@ sample_networks = {
</relation>
</osm>
""",
"station_count": 4,
"num_stations": 4,
"tracks": [],
"extended_tracks": [
(0.0, 0.0),
@ -1455,7 +1455,7 @@ sample_networks = {
</relation>
</osm>
""",
"station_count": 4,
"num_stations": 4,
"tracks": [
(0.0, 0.0),
(0.0, 1.0),

View file

@ -0,0 +1,316 @@
sample_networks = {
"No errors": {
"xml": """<?xml version='1.0' encoding='UTF-8'?>
<osm version='0.6' generator='JOSM'>
<node id='1' version='1' lat='0.0' lon='0.0'>
<tag k='name' v='Station 1' />
<tag k='railway' v='station' />
<tag k='station' v='subway' />
</node>
<node id='2' version='1' lat='0.0' lon='1.0'>
<tag k='name' v='Station 2' />
<tag k='railway' v='station' />
<tag k='station' v='subway' />
</node>
<relation id='1' version='1'>
<member type='node' ref='1' role='' />
<member type='node' ref='2' role='' />
<tag k='name' v='Forward' />
<tag k='ref' v='1' />
<tag k='route' v='subway' />
<tag k='type' v='route' />
</relation>
<relation id='2' version='1'>
<member type='node' ref='2' role='' />
<member type='node' ref='1' role='' />
<tag k='name' v='Backward' />
<tag k='ref' v='1' />
<tag k='route' v='subway' />
<tag k='type' v='route' />
</relation>
<relation id='3' version='1'>
<member type='relation' ref='1' role='' />
<member type='relation' ref='2' role='' />
<tag k='ref' v='1' />
<tag k='colour' v='red' />
<tag k='route_master' v='subway' />
<tag k='type' v='route_master' />
</relation>
</osm>
""",
"num_stations": 2,
"num_lines": 1,
"num_light_lines": 0,
"num_interchanges": 0,
"errors": [],
"warnings": [],
"notices": [],
},
"Bad station order": {
"xml": """<?xml version='1.0' encoding='UTF-8'?>
<osm version='0.6' generator='JOSM'>
<node id='1' version='1' lat='0.0' lon='0.0'>
<tag k='name' v='Station 1' />
<tag k='railway' v='station' />
<tag k='station' v='subway' />
</node>
<node id='2' version='1' lat='0.0' lon='1.0'>
<tag k='name' v='Station 2' />
<tag k='railway' v='station' />
<tag k='station' v='subway' />
</node>
<node id='3' version='1' lat='0.0' lon='2.0'>
<tag k='name' v='Station 3' />
<tag k='railway' v='station' />
<tag k='station' v='subway' />
</node>
<node id='4' version='1' lat='0.0' lon='3.0'>
<tag k='name' v='Station 4' />
<tag k='railway' v='station' />
<tag k='station' v='subway' />
</node>
<relation id='1' version='1'>
<member type='node' ref='1' role='' />
<member type='node' ref='3' role='' />
<member type='node' ref='2' role='' />
<member type='node' ref='4' role='' />
<tag k='name' v='Forward' />
<tag k='ref' v='1' />
<tag k='route' v='subway' />
<tag k='type' v='route' />
</relation>
<relation id='2' version='1'>
<member type='node' ref='4' role='' />
<member type='node' ref='3' role='' />
<member type='node' ref='2' role='' />
<member type='node' ref='1' role='' />
<tag k='name' v='Backward' />
<tag k='ref' v='1' />
<tag k='route' v='subway' />
<tag k='type' v='route' />
</relation>
<relation id='3' version='1'>
<member type='relation' ref='1' role='' />
<member type='relation' ref='2' role='' />
<tag k='ref' v='1' />
<tag k='colour' v='red' />
<tag k='route_master' v='subway' />
<tag k='type' v='route_master' />
</relation>
</osm>
""",
"num_stations": 4,
"num_lines": 1,
"num_light_lines": 0,
"num_interchanges": 0,
"errors": [
'Angle between stops around "Station 3" (2.0, 0.0) '
'is too narrow, 0 degrees (relation 1, "Forward")',
'Angle between stops around "Station 2" (1.0, 0.0) '
'is too narrow, 0 degrees (relation 1, "Forward")',
],
"warnings": [],
"notices": [],
},
"Angle < 20 degrees": {
"xml": """<?xml version='1.0' encoding='UTF-8'?>
<osm version='0.6' generator='JOSM'>
<node id='1' version='1' lat='0.0' lon='0.0'>
<tag k='name' v='Station 1' />
<tag k='railway' v='station' />
<tag k='station' v='subway' />
</node>
<node id='2' version='1' lat='0.0' lon='1.0'>
<tag k='name' v='Station 2' />
<tag k='railway' v='station' />
<tag k='station' v='subway' />
</node>
<node id='3' version='1' lat='0.2' lon='0.0'>
<tag k='name' v='Station 3' />
<tag k='railway' v='station' />
<tag k='station' v='subway' />
</node>
<relation id='1' version='1'>
<member type='node' ref='1' role='' />
<member type='node' ref='2' role='' />
<member type='node' ref='3' role='' />
<tag k='name' v='Forward' />
<tag k='ref' v='1' />
<tag k='route' v='subway' />
<tag k='type' v='route' />
</relation>
<relation id='2' version='1'>
<member type='node' ref='3' role='' />
<member type='node' ref='2' role='' />
<member type='node' ref='1' role='' />
<member type='way' ref='1' role='' />
<tag k='name' v='Backward' />
<tag k='ref' v='1' />
<tag k='route' v='subway' />
<tag k='type' v='route' />
</relation>
<relation id='3' version='1'>
<member type='relation' ref='1' role='' />
<member type='relation' ref='2' role='' />
<tag k='ref' v='1' />
<tag k='colour' v='red' />
<tag k='route_master' v='subway' />
<tag k='type' v='route_master' />
</relation>
</osm>
""",
"num_stations": 3,
"num_lines": 1,
"num_light_lines": 0,
"num_interchanges": 0,
"errors": [
'Angle between stops around "Station 2" (1.0, 0.0) '
'is too narrow, 11 degrees (relation 1, "Forward")',
'Angle between stops around "Station 2" (1.0, 0.0) '
'is too narrow, 11 degrees (relation 2, "Backward")',
],
"warnings": [],
"notices": [],
},
"Angle between 20 and 45 degrees": {
"xml": """<?xml version='1.0' encoding='UTF-8'?>
<osm version='0.6' generator='JOSM'>
<node id='1' version='1' lat='0.0' lon='0.0'>
<tag k='name' v='Station 1' />
<tag k='railway' v='station' />
<tag k='station' v='subway' />
</node>
<node id='2' version='1' lat='0.0' lon='1.0'>
<tag k='name' v='Station 2' />
<tag k='railway' v='station' />
<tag k='station' v='subway' />
</node>
<node id='3' version='1' lat='0.5' lon='0.0'>
<tag k='name' v='Station 3' />
<tag k='railway' v='station' />
<tag k='station' v='subway' />
</node>
<relation id='1' version='1'>
<member type='node' ref='1' role='' />
<member type='node' ref='2' role='' />
<member type='node' ref='3' role='' />
<tag k='name' v='Forward' />
<tag k='ref' v='1' />
<tag k='route' v='subway' />
<tag k='type' v='route' />
</relation>
<relation id='2' version='1'>
<member type='node' ref='3' role='' />
<member type='node' ref='2' role='' />
<member type='node' ref='1' role='' />
<member type='way' ref='1' role='' />
<tag k='name' v='Backward' />
<tag k='ref' v='1' />
<tag k='route' v='subway' />
<tag k='type' v='route' />
</relation>
<relation id='3' version='1'>
<member type='relation' ref='1' role='' />
<member type='relation' ref='2' role='' />
<tag k='ref' v='1' />
<tag k='colour' v='red' />
<tag k='route_master' v='subway' />
<tag k='type' v='route_master' />
</relation>
</osm>
""",
"num_stations": 3,
"num_lines": 1,
"num_light_lines": 0,
"num_interchanges": 0,
"errors": [],
"warnings": [],
"notices": [
'Angle between stops around "Station 2" (1.0, 0.0) '
'is too narrow, 27 degrees (relation 1, "Forward")',
'Angle between stops around "Station 2" (1.0, 0.0) '
'is too narrow, 27 degrees (relation 2, "Backward")',
],
},
"Stops unordered along tracks provided each angle > 45 degrees": {
"xml": """<?xml version='1.0' encoding='UTF-8'?>
<osm version='0.6' generator='JOSM'>
<node id='1' version='1' lat='0.0' lon='0.0'>
<tag k='name' v='Station 1' />
<tag k='railway' v='station' />
<tag k='station' v='subway' />
</node>
<node id='2' version='1' lat='0.0' lon='1.0'>
<tag k='name' v='Station 2' />
<tag k='railway' v='station' />
<tag k='station' v='subway' />
</node>
<node id='3' version='1' lat='0.5' lon='0.0'>
<tag k='name' v='Station 3' />
<tag k='railway' v='station' />
<tag k='station' v='subway' />
</node>
<node id='4' version='1' lat='1.0' lon='1.0'>
<tag k='name' v='Station 4' />
<tag k='railway' v='station' />
<tag k='station' v='subway' />
</node>
<way id='1' version='1'>
<nd ref='1' />
<nd ref='2' />
<nd ref='3' />
<tag k='railway' v='subway' />
</way>
<way id='2' version='1'>
<nd ref='3' />
<nd ref='4' />
<tag k='railway' v='subway' />
</way>
<relation id='1' version='1'>
<member type='node' ref='1' role='' />
<member type='node' ref='3' role='' />
<member type='node' ref='2' role='' />
<member type='node' ref='4' role='' />
<member type='way' ref='1' role='' />
<member type='way' ref='2' role='' />
<tag k='name' v='Forward' />
<tag k='ref' v='1' />
<tag k='route' v='subway' />
<tag k='type' v='route' />
</relation>
<relation id='2' version='1'>
<member type='node' ref='4' role='' />
<member type='node' ref='2' role='' />
<member type='node' ref='3' role='' />
<member type='node' ref='1' role='' />
<member type='way' ref='2' role='' />
<member type='way' ref='1' role='' />
<tag k='name' v='Backward' />
<tag k='ref' v='1' />
<tag k='route' v='subway' />
<tag k='type' v='route' />
</relation>
<relation id='3' version='1'>
<member type='relation' ref='1' role='' />
<member type='relation' ref='2' role='' />
<tag k='ref' v='1' />
<tag k='colour' v='red' />
<tag k='route_master' v='subway' />
<tag k='type' v='route_master' />
</relation>
</osm>
""",
"num_stations": 4,
"num_lines": 1,
"num_light_lines": 0,
"num_interchanges": 0,
"errors": [
'Stops on tracks are unordered near "Station 2" (1.0, 0.0) '
'(relation 1, "Forward")',
'Stops on tracks are unordered near "Station 3" (0.0, 0.5) '
'(relation 2, "Backward")',
],
"warnings": [],
"notices": [],
},
}

View file

@ -9,52 +9,16 @@ or simply
> python -m unittest
"""
import io
import unittest
from subway_structure import City
from subway_io import load_xml
from tests.sample_data import sample_networks
from tests.sample_data_for_build_tracks import sample_networks
from tests.util import TestCase
class TestOneRouteTracks(unittest.TestCase):
class TestOneRouteTracks(TestCase):
"""Test tracks extending and truncating on one-route networks"""
CITY_TEMPLATE = {
"id": 1,
"name": "Null Island",
"country": "World",
"continent": "Africa",
"num_stations": None, # Would be taken from the sample network data
"num_lines": 1,
"num_light_lines": 0,
"num_interchanges": 0,
"bbox": "-179, -89, 179, 89",
"networks": "",
}
def assertListAlmostEqual(self, list1, list2, places=10) -> None:
if not (isinstance(list1, list) and isinstance(list2, list)):
raise RuntimeError(
f"Not lists passed to the '{self.__class__.__name__}."
"assertListAlmostEqual' method"
)
self.assertEqual(len(list1), len(list2))
for a, b in zip(list1, list2):
if isinstance(a, list) and isinstance(b, list):
self.assertListAlmostEqual(a, b, places)
else:
self.assertAlmostEqual(a, b, places)
def prepare_city_routes(self, network) -> tuple:
city_data = self.CITY_TEMPLATE.copy()
city_data["num_stations"] = network["station_count"]
city = City(city_data)
elements = load_xml(io.BytesIO(network["xml"].encode("utf-8")))
for el in elements:
city.add(el)
city.extract_routes()
city.validate()
city = self.validate_city(network)
self.assertTrue(city.is_good)

View file

@ -0,0 +1,22 @@
from tests.sample_data_for_error_messages import sample_networks
from tests.util import TestCase
class TestValidationMessages(TestCase):
"""Test that the validator provides expected validation messages
on different types of errors in input OSM data.
"""
def _test_validation_messages_for_network(self, network_data):
city = self.validate_city(network_data)
for err_level in ("errors", "warnings", "notices"):
self.assertListEqual(
sorted(getattr(city, err_level)),
sorted(network_data[err_level]),
)
def test_validation_messages(self) -> None:
for network_name, network_data in sample_networks.items():
with self.subTest(msg=network_name):
self._test_validation_messages_for_network(network_data)

View file

@ -0,0 +1,36 @@
import inspect
from pathlib import Path
from unittest import TestCase
from process_subways import prepare_cities
class TestPrepareCities(TestCase):
def test_prepare_cities(self) -> None:
csv_path = (
Path(inspect.getfile(self.__class__)).parent
/ "assets"
/ "networks_with_bad_values.csv"
)
cities = prepare_cities(cities_info_url=f"file://{csv_path}")
city_errors = {city.name: sorted(city.errors) for city in cities}
expected_errors = {
"Nizhny Novgorod": [],
"Novosibirsk": ["Configuration error: wrong value for id: NBS"],
"Saint Petersburg": [],
"Samara": [
"Configuration error: wrong value for num_stations: 10x"
],
"Volgograd": [
"Configuration error: wrong value for num_light_lines: 2zero",
"Configuration error: wrong value for num_lines: zero",
],
"Yekaterinburg": [
"Configuration error: wrong value for num_stations: <empty>"
],
}
self.assertDictEqual(city_errors, expected_errors)

49
tests/util.py Normal file
View file

@ -0,0 +1,49 @@
import io
from unittest import TestCase as unittestTestCase
from subway_io import load_xml
from subway_structure import City
class TestCase(unittestTestCase):
"""TestCase class for testing the Subway Validator"""
CITY_TEMPLATE = {
"id": 1,
"name": "Null Island",
"country": "World",
"continent": "Africa",
"bbox": "-179, -89, 179, 89",
"networks": "",
"num_stations": None,
"num_lines": 1,
"num_light_lines": 0,
"num_interchanges": 0,
}
def validate_city(self, network) -> City:
city_data = self.CITY_TEMPLATE.copy()
for attr in self.CITY_TEMPLATE.keys():
if attr in network:
city_data[attr] = network[attr]
city = City(city_data)
elements = load_xml(io.BytesIO(network["xml"].encode("utf-8")))
for el in elements:
city.add(el)
city.extract_routes()
city.validate()
return city
def assertListAlmostEqual(self, list1, list2, places=10) -> None:
if not (isinstance(list1, list) and isinstance(list2, list)):
raise RuntimeError(
f"Not lists passed to the '{self.__class__.__name__}."
"assertListAlmostEqual' method"
)
self.assertEqual(len(list1), len(list2))
for a, b in zip(list1, list2):
if isinstance(a, list) and isinstance(b, list):
self.assertListAlmostEqual(a, b, places)
else:
self.assertAlmostEqual(a, b, places)