Fix calculating stop positions for route with rails reversed relative to stops order

This commit is contained in:
Alexey Zakharenkov 2022-11-04 22:11:57 +03:00 committed by Alexey Zakharenkov
parent a6b76068f8
commit 921ca513cf
3 changed files with 266 additions and 56 deletions

View file

@ -757,27 +757,31 @@ class Route:
def project_stops_on_line(self):
projected, stop_near_tracks_criterion = self.get_stop_projections()
self.first_stop_on_rails_index = 0
projected_stops_data = {
'first_stop_on_rails_index': None,
'last_stop_on_rails_index': None,
'stops_on_longest_line': [], # list [{'route_stop': RouteStop,
# 'coords': (lon, lat),
# 'positions_on_rails': [] }
}
first_index = 0
while (
self.first_stop_on_rails_index < len(self.stops)
and not stop_near_tracks_criterion(self.first_stop_on_rails_index)
first_index < len(self.stops)
and not stop_near_tracks_criterion(first_index)
):
self.first_stop_on_rails_index += 1
first_index += 1
projected_stops_data['first_stop_on_rails_index'] = first_index
self.last_stop_on_rails_index = len(self.stops) - 1
last_index = len(self.stops) - 1
while (
self.last_stop_on_rails_index > self.first_stop_on_rails_index
and not stop_near_tracks_criterion(self.last_stop_on_rails_index)
last_index > projected_stops_data['first_stop_on_rails_index']
and not stop_near_tracks_criterion(last_index)
):
self.last_stop_on_rails_index -= 1
last_index -= 1
projected_stops_data['last_stop_on_rails_index'] = last_index
stops_on_longest_line = []
for i, route_stop in enumerate(self.stops):
if not (
self.first_stop_on_rails_index
<= i
<= self.last_stop_on_rails_index
):
if not first_index <= i <= last_index:
continue
if projected[i]['projected_point'] is None:
@ -788,6 +792,11 @@ class Route:
self.element,
)
else:
stop_data = {
'route_stop': route_stop,
'coords': None,
'positions_on_rails': None,
}
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.
@ -800,12 +809,12 @@ class Route:
self.element,
)
else:
route_stop.stop = projected_point
route_stop.positions_on_rails = projected[i][
stop_data['coords'] = projected_point
stop_data['positions_on_rails'] = projected[i][
'positions_on_line'
]
stops_on_longest_line.append(route_stop)
return stops_on_longest_line
projected_stops_data['stops_on_longest_line'].append(stop_data)
return projected_stops_data
def calculate_distances(self):
dist = 0
@ -1072,10 +1081,24 @@ class Route:
self.element
)
stops_on_longest_line = self.project_stops_on_line()
self.check_and_recover_stops_order(stops_on_longest_line)
projected_stops_data = self.project_stops_on_line()
self.check_and_recover_stops_order(projected_stops_data)
self.apply_projected_stops_data(projected_stops_data)
self.calculate_distances()
def apply_projected_stops_data(self, projected_stops_data: dict) -> None:
"""Store better stop coordinates and indexes of first/last stops
that lie on a continuous track line, to the instance attributes.
"""
for attr in ('first_stop_on_rails_index', 'last_stop_on_rails_index'):
setattr(self, attr, projected_stops_data[attr])
for stop_data in projected_stops_data['stops_on_longest_line']:
route_stop = stop_data['route_stop']
route_stop.positions_on_rails = stop_data['positions_on_rails']
if stop_coords := stop_data['coords']:
route_stop.stop = stop_coords
def get_extended_tracks(self):
"""Amend tracks with points of leading/trailing self.stops
that were not projected onto the longest tracks line.
@ -1156,34 +1179,15 @@ class Route:
def check_stops_order_on_tracks_direct(self, stop_sequence):
"""Checks stops order on tracks, following stop_sequence
in direct order only.
:param stop_sequence: list of RouteStop that belong to the
longest contiguous sequence of tracks in a route.
:param stop_sequence: list of dict{'route_stop', 'positions_on_rails',
'coords'} for RouteStops that belong to the 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' "
"attribute in route {}".format(
route_stop.stoparea.id,
route_stop.stoparea.name,
"no" if error_type == 1 else "empty",
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)
positions_on_rails = route_stop.positions_on_rails
assert positions_on_rails, make_assertion_error_msg(
route_stop, error_type=2
)
for stop_data in stop_sequence:
positions_on_rails = stop_data['positions_on_rails']
suitable_occurrence = 0
while (
suitable_occurrence < len(positions_on_rails)
@ -1196,22 +1200,26 @@ class Route:
suitable_occurrence -= 1
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
)
max_position_on_rails = positions_on_rails[suitable_occurrence]
def check_stops_order_on_tracks(self, stop_sequence):
def check_stops_order_on_tracks(self, projected_stops_data):
"""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.
:param projected_stops_data: info about RouteStops that belong to the
longest contiguous sequence of tracks in a route. May be changed
if tracks reversing is performed.
:return: error message on the first order violation or None.
"""
error_message = self.check_stops_order_on_tracks_direct(stop_sequence)
error_message = self.check_stops_order_on_tracks_direct(
projected_stops_data['stops_on_longest_line']
)
if error_message:
error_message_reversed = self.check_stops_order_on_tracks_direct(
reversed(stop_sequence)
reversed(projected_stops_data['stops_on_longest_line'])
)
if error_message_reversed is None:
error_message = None
@ -1220,15 +1228,18 @@ class Route:
self.element,
)
self.tracks.reverse()
new_projected_stops_data = self.project_stops_on_line()
projected_stops_data.update(new_projected_stops_data)
return error_message
def check_stops_order(self, stops_on_longest_line):
def check_stops_order(self, projected_stops_data):
(
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
projected_stops_data
)
disorder_warnings = angle_disorder_warnings
disorder_errors = angle_disorder_errors
@ -1236,9 +1247,12 @@ class Route:
disorder_errors.append(disorder_on_tracks_error)
return disorder_warnings, disorder_errors
def check_and_recover_stops_order(self, stops_on_longest_line):
def check_and_recover_stops_order(self, projected_stops_data: dict):
"""
:param projected_stops_data: may change if we need to reverse tracks
"""
disorder_warnings, disorder_errors = self.check_stops_order(
stops_on_longest_line
projected_stops_data
)
if disorder_warnings or disorder_errors:
resort_success = False

View file

@ -44,6 +44,16 @@ sample_networks = {
(1.0, 0.0),
],
"truncated_tracks": [],
"forward": {
"first_stop_on_rails_index": 2,
"last_stop_on_rails_index": 1,
"positions_on_rails": [],
},
"backward": {
"first_stop_on_rails_index": 2,
"last_stop_on_rails_index": 1,
"positions_on_rails": [],
},
},
"Only 2 stations connected with rails": {
@ -104,6 +114,16 @@ sample_networks = {
(0.0, 0.0),
(1.0, 0.0),
],
"forward": {
"first_stop_on_rails_index": 0,
"last_stop_on_rails_index": 1,
"positions_on_rails": [[0], [1]],
},
"backward": {
"first_stop_on_rails_index": 0,
"last_stop_on_rails_index": 1,
"positions_on_rails": [[0], [1]],
},
},
"Only 6 stations, no rails": {
@ -183,6 +203,16 @@ sample_networks = {
(5.0, 0.0),
],
"truncated_tracks": [],
"forward": {
"first_stop_on_rails_index": 6,
"last_stop_on_rails_index": 5,
"positions_on_rails": [],
},
"backward": {
"first_stop_on_rails_index": 6,
"last_stop_on_rails_index": 5,
"positions_on_rails": [],
},
},
"One rail line connecting all stations": {
@ -287,6 +317,16 @@ sample_networks = {
(4.0, 0.0),
(5.0, 0.0),
],
"forward": {
"first_stop_on_rails_index": 0,
"last_stop_on_rails_index": 5,
"positions_on_rails": [[0], [1], [2], [3], [4], [5]],
},
"backward": {
"first_stop_on_rails_index": 0,
"last_stop_on_rails_index": 5,
"positions_on_rails": [[0], [1], [2], [3], [4], [5]],
},
},
"One rail line connecting all stations except the last": {
@ -388,6 +428,16 @@ sample_networks = {
(3.0, 0.0),
(4.0, 0.0),
],
"forward": {
"first_stop_on_rails_index": 0,
"last_stop_on_rails_index": 4,
"positions_on_rails": [[0], [1], [2], [3], [4]],
},
"backward": {
"first_stop_on_rails_index": 1,
"last_stop_on_rails_index": 5,
"positions_on_rails": [[0], [1], [2], [3], [4]],
},
},
"One rail line connecting all stations except the fist": {
@ -489,6 +539,16 @@ sample_networks = {
(4.0, 0.0),
(5.0, 0.0),
],
"forward": {
"first_stop_on_rails_index": 1,
"last_stop_on_rails_index": 5,
"positions_on_rails": [[0], [1], [2], [3], [4]],
},
"backward": {
"first_stop_on_rails_index": 0,
"last_stop_on_rails_index": 4,
"positions_on_rails": [[0], [1], [2], [3], [4]],
},
},
"One rail line connecting all stations except the fist and the last": {
@ -587,6 +647,16 @@ sample_networks = {
(3.0, 0.0),
(4.0, 0.0),
],
"forward": {
"first_stop_on_rails_index": 1,
"last_stop_on_rails_index": 4,
"positions_on_rails": [[0], [1], [2], [3]],
},
"backward": {
"first_stop_on_rails_index": 1,
"last_stop_on_rails_index": 4,
"positions_on_rails": [[0], [1], [2], [3]],
},
},
"One rail line connecting only 2 first stations": {
@ -679,6 +749,16 @@ sample_networks = {
(0.0, 0.0),
(1.0, 0.0),
],
"forward": {
"first_stop_on_rails_index": 0,
"last_stop_on_rails_index": 1,
"positions_on_rails": [[0], [1]],
},
"backward": {
"first_stop_on_rails_index": 4,
"last_stop_on_rails_index": 5,
"positions_on_rails": [[0], [1]],
},
},
"One rail line connecting only 2 last stations": {
@ -771,6 +851,16 @@ sample_networks = {
(4.0, 0.0),
(5.0, 0.0),
],
"forward": {
"first_stop_on_rails_index": 4,
"last_stop_on_rails_index": 5,
"positions_on_rails": [[0], [1]],
},
"backward": {
"first_stop_on_rails_index": 0,
"last_stop_on_rails_index": 1,
"positions_on_rails": [[0], [1]],
},
},
"One rail connecting all stations and protruding at both ends": {
@ -885,6 +975,16 @@ sample_networks = {
(4.0, 0.0),
(5.0, 0.0),
],
"forward": {
"first_stop_on_rails_index": 0,
"last_stop_on_rails_index": 5,
"positions_on_rails": [[1], [2], [3], [4], [5], [6]],
},
"backward": {
"first_stop_on_rails_index": 0,
"last_stop_on_rails_index": 5,
"positions_on_rails": [[1], [2], [3], [4], [5], [6]],
},
},
"Several rails with reversed order for backward route, connecting all stations and protruding at both ends": {
@ -1005,6 +1105,16 @@ sample_networks = {
(4.0, 0.0),
(5.0, 0.0),
],
"forward": {
"first_stop_on_rails_index": 0,
"last_stop_on_rails_index": 5,
"positions_on_rails": [[1], [2], [3], [4], [5], [6]],
},
"backward": {
"first_stop_on_rails_index": 0,
"last_stop_on_rails_index": 5,
"positions_on_rails": [[1], [2], [3], [4], [5], [6]],
},
},
"One rail laying near all stations requiring station projecting, protruding at both ends": {
@ -1097,6 +1207,16 @@ sample_networks = {
(0.0, 0.0),
(5.0, 0.0),
],
"forward": {
"first_stop_on_rails_index": 0,
"last_stop_on_rails_index": 5,
"positions_on_rails": [[1/7], [2/7], [3/7], [4/7], [5/7], [6/7]],
},
"backward": {
"first_stop_on_rails_index": 0,
"last_stop_on_rails_index": 5,
"positions_on_rails": [[1/7], [2/7], [3/7], [4/7], [5/7], [6/7]],
},
},
"One rail laying near all stations except the first and last": {
@ -1191,6 +1311,16 @@ sample_networks = {
(1.0, 0.0),
(4.0, 0.0),
],
"forward": {
"first_stop_on_rails_index": 1,
"last_stop_on_rails_index": 4,
"positions_on_rails": [[0], [1/3], [2/3], [1]],
},
"backward": {
"first_stop_on_rails_index": 1,
"last_stop_on_rails_index": 4,
"positions_on_rails": [[0], [1/3], [2/3], [1]],
},
},
"Circle route without rails": {
@ -1250,6 +1380,16 @@ sample_networks = {
(0.0, 0.0),
],
"truncated_tracks": [],
"forward": {
"first_stop_on_rails_index": 5,
"last_stop_on_rails_index": 4,
"positions_on_rails": [],
},
"backward": {
"first_stop_on_rails_index": 5,
"last_stop_on_rails_index": 4,
"positions_on_rails": [],
},
},
"Circle route with closed rail line connecting all stations": {
@ -1331,5 +1471,15 @@ sample_networks = {
(1.0, 0.0),
(0.0, 0.0),
],
"forward": {
"first_stop_on_rails_index": 0,
"last_stop_on_rails_index": 4,
"positions_on_rails": [[0, 4], [1], [2], [3], [0, 4]],
},
"backward": {
"first_stop_on_rails_index": 0,
"last_stop_on_rails_index": 4,
"positions_on_rails": [[0, 4], [1], [2], [3], [0, 4]],
},
},
}

View file

@ -33,7 +33,20 @@ class TestOneRouteTracks(unittest.TestCase):
"networks": "",
}
def prepare_city_routes(self, network):
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)
@ -96,12 +109,45 @@ class TestOneRouteTracks(unittest.TestCase):
"Wrong backward tracks after truncating",
)
def test_tracks_extending(self):
def _test_stop_positions_on_rails_for_network(self, network_data):
fwd_route, bwd_route = self.prepare_city_routes(network_data)
for route, route_label in zip(
(fwd_route, bwd_route), ("forward", "backward")
):
route_data = network_data[route_label]
for attr in (
"first_stop_on_rails_index",
"last_stop_on_rails_index",
):
self.assertEqual(
getattr(route, attr),
route_data[attr],
f"Wrong {attr} for {route_label} route",
)
first_index = route_data["first_stop_on_rails_index"]
last_index = route_data["last_stop_on_rails_index"]
positions_on_rails = [
rs.positions_on_rails
for rs in route.stops[first_index : last_index + 1]
]
self.assertListAlmostEqual(
positions_on_rails, route_data["positions_on_rails"]
)
def test_tracks_extending(self) -> None:
for network_name, network_data in sample_networks.items():
with self.subTest(msg=network_name):
self._test_tracks_extending_for_network(network_data)
def test_tracks_truncating(self):
def test_tracks_truncating(self) -> None:
for network_name, network_data in sample_networks.items():
with self.subTest(msg=network_name):
self._test_tracks_truncating_for_network(network_data)
def test_stop_position_on_rails(self) -> None:
for network_name, network_data in sample_networks.items():
with self.subTest(msg=network_name):
self._test_stop_positions_on_rails_for_network(network_data)