Fix calculating stop positions for route with rails reversed relative to stops order
This commit is contained in:
parent
a6b76068f8
commit
921ca513cf
3 changed files with 266 additions and 56 deletions
|
@ -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
|
||||
|
|
|
@ -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]],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Reference in a new issue