diff --git a/cmake/SimutransSourceList.cmake b/cmake/SimutransSourceList.cmake
index 33d3044db..607f2fe26 100644
--- a/cmake/SimutransSourceList.cmake
+++ b/cmake/SimutransSourceList.cmake
@@ -268,6 +268,7 @@ target_sources(simutrans PRIVATE
 		src/simutrans/script/api/api_player.cc
 		src/simutrans/script/api/api_scenario.cc
 		src/simutrans/script/api/api_schedule.cc
+		src/simutrans/script/api/api_schedule_route_test.cc
 		src/simutrans/script/api/api_settings.cc
 		src/simutrans/script/api/api_simple.cc
 		src/simutrans/script/api/api_tiles.cc
diff --git a/src/simutrans/script/api/api.h b/src/simutrans/script/api/api.h
index bcc4d9068..46e0000b2 100644
--- a/src/simutrans/script/api/api.h
+++ b/src/simutrans/script/api/api.h
@@ -32,6 +32,8 @@ void export_world(HSQUIRRELVM vm, bool scenario);
 
 void export_pathfinding(HSQUIRRELVM vm);
 
+void export_schedule_route_test(HSQUIRRELVM vm); // TEST SUPPORT ONLY
+
 void export_global_constants(HSQUIRRELVM vm);
 
 
diff --git a/src/simutrans/script/export_objs.cc b/src/simutrans/script/export_objs.cc
index 8967d6397..e0495a119 100644
--- a/src/simutrans/script/export_objs.cc
+++ b/src/simutrans/script/export_objs.cc
@@ -39,6 +39,9 @@ void register_export_function(HSQUIRRELVM vm, bool scenario)
 	export_tiles(vm);
 	export_world(vm, scenario);
 	export_pathfinding(vm);
+	if (scenario) {
+		export_schedule_route_test(vm); // TEST SUPPORT ONLY
+	}
 
 	export_commands(vm);
 
diff --git a/tests/all_tests.nut b/tests/all_tests.nut
index e116a0cb9..dfed5a641 100644
--- a/tests/all_tests.nut
+++ b/tests/all_tests.nut
@@ -23,6 +23,7 @@ include("tests/test_label")
 include("tests/test_powerline")
 include("tests/test_reservation")
 include("tests/test_scenario")
+include("tests/test_schedule_route")
 include("tests/test_sign")
 include("tests/test_slope")
 include("tests/test_terraform")
@@ -242,5 +243,21 @@ all_tests <- [
 	test_wayobj_build_disconnected,
 	test_wayobj_upgrade_downgrade,
 	test_wayobj_upgrade_change_owner,
-	test_wayobj_electrify_depot
+	test_wayobj_electrify_depot,
+	test_schedule_route_open_and_close,
+	test_schedule_route_update_on_change,
+	test_schedule_route_second_editor,
+	test_schedule_route_outdated_request,
+	test_schedule_route_broken_leg,
+	test_schedule_route_rail,
+	test_schedule_route_monorail,
+	test_schedule_route_water,
+	test_schedule_route_line_without_convoys,
+	test_schedule_route_convoy_and_line,
+	test_schedule_route_private_way,
+	test_schedule_route_rotate,
+	test_schedule_route_electric_follows_catenary,
+	test_schedule_route_diesel_ignores_catenary,
+	test_schedule_route_electric_without_catenary,
+	test_schedule_route_electric_survives_schedule_change
 ]
diff --git a/src/simutrans/script/api/api_schedule_route_test.cc b/src/simutrans/script/api/api_schedule_route_test.cc
new file mode 100644
index 000000000..c96044616
--- /dev/null
+++ b/src/simutrans/script/api/api_schedule_route_test.cc
@@ -0,0 +1,312 @@
+/*
+ * This file is part of the Simutrans project under the Artistic License.
+ * (see LICENSE.txt)
+ */
+
+/** @file api_schedule_route_test.cc
+ *
+ * TEST SUPPORT ONLY - not part of the schedule route overlay itself.
+ *
+ * Drives the real schedule editor component (gui_schedule_t) and reads the real
+ * display only route channel of the world, so that the automated tests exercise
+ * the production code path instead of a copy of it.
+ */
+
+#include "api.h"
+
+#include "../api_class.h"
+#include "../api_function.h"
+
+#include "../../dataobj/schedule.h"
+#include "../../ground/grund.h"
+#include "../../display/simgraph.h"
+#include "../../display/viewport.h"
+#include "../../gui/components/gui_schedule.h"
+#include "../../obj/simobj.h"
+#include "../../pathes.h"
+#include "../../player/simplay.h"
+#include "../../simconvoi.h"
+#include "../../simline.h"
+#include "../../world/simworld.h"
+
+// for access(), same block as simtool.cc
+#ifdef _MSC_VER
+#	include <io.h>
+#	define W_OK 2
+#else
+#	include <unistd.h>
+#endif
+
+using namespace script_api;
+
+
+#define SROUTE_SLOTS (3)
+
+/// one open schedule editor per slot, so that two windows can be tested against each other
+static gui_schedule_t *sroute_gui[SROUTE_SLOTS] = { NULL, NULL, NULL };
+static player_t       *sroute_player_of[SROUTE_SLOTS] = { NULL, NULL, NULL };
+static convoihandle_t  sroute_cnv_of[SROUTE_SLOTS];
+static linehandle_t    sroute_lin_of[SROUTE_SLOTS];
+
+
+static gui_schedule_t *sroute_slot(HSQUIRRELVM vm, SQInteger index)
+{
+	const uint8 slot = param<uint8>::get(vm, index);
+	return slot < SROUTE_SLOTS ? sroute_gui[slot] : NULL;
+}
+
+
+static void sroute_fill(uint8 slot, schedule_t *sched, player_t *pl, convoihandle_t cnv, linehandle_t lin)
+{
+	delete sroute_gui[slot];
+	sroute_gui[slot] = new gui_schedule_t();
+	sroute_player_of[slot] = pl;
+	sroute_cnv_of[slot] = cnv;
+	sroute_lin_of[slot] = lin;
+	sroute_gui[slot]->init( sched, pl, cnv, lin, true );
+}
+
+
+static SQInteger sroute_open(HSQUIRRELVM vm) // slot, schedule_x, player_nr
+{
+	const uint8 slot = param<uint8>::get(vm, 2);
+	schedule_t *sched = param<schedule_t*>::get(vm, 3);
+	player_t *pl = welt->get_player( param<uint8>::get(vm, 4) );
+	if(  slot >= SROUTE_SLOTS  ||  sched == NULL  ||  pl == NULL  ) {
+		return sq_raise_error(vm, "sroute_open: bad slot, schedule or player");
+	}
+	sroute_fill( slot, sched, pl, convoihandle_t(), linehandle_t() );
+	return 0;
+}
+
+
+static SQInteger sroute_open_line(HSQUIRRELVM vm) // slot, line_x
+{
+	const uint8 slot = param<uint8>::get(vm, 2);
+	linehandle_t lin = param<linehandle_t>::get(vm, 3);
+	if(  slot >= SROUTE_SLOTS  ||  !lin.is_bound()  ) {
+		return sq_raise_error(vm, "sroute_open_line: bad slot or line");
+	}
+	sroute_fill( slot, lin->get_schedule(), lin->get_owner(), convoihandle_t(), lin );
+	return 0;
+}
+
+
+static SQInteger sroute_open_convoy(HSQUIRRELVM vm) // slot, convoy_x
+{
+	const uint8 slot = param<uint8>::get(vm, 2);
+	convoihandle_t cnv = param<convoihandle_t>::get(vm, 3);
+	if(  slot >= SROUTE_SLOTS  ||  !cnv.is_bound()  ||  cnv->get_schedule() == NULL  ) {
+		return sq_raise_error(vm, "sroute_open_convoy: bad slot or convoy");
+	}
+	sroute_fill( slot, cnv->get_schedule(), cnv->get_owner(), cnv, linehandle_t() );
+	return 0;
+}
+
+
+static SQInteger sroute_mark(HSQUIRRELVM vm) // slot, bool
+{
+	gui_schedule_t *gui = sroute_slot(vm, 2);
+	if(  gui == NULL  ) {
+		return sq_raise_error(vm, "sroute_mark: empty slot");
+	}
+	gui->highlight_schedule( param<bool>::get(vm, 3) );
+	return 0;
+}
+
+
+static SQInteger sroute_reinit(HSQUIRRELVM vm) // slot, schedule_x
+{
+	// the editor is re-initialised with a changed schedule, as line_management_gui
+	// does when the schedule of the shown line changed. The component - and with it
+	// the overlay id - stays the same, only its content changes.
+	const uint8 slot = param<uint8>::get(vm, 2);
+	schedule_t *sched = param<schedule_t*>::get(vm, 3);
+	if(  slot >= SROUTE_SLOTS  ||  sroute_gui[slot] == NULL  ||  sched == NULL  ) {
+		return sq_raise_error(vm, "sroute_reinit: empty slot or bad schedule");
+	}
+	sroute_gui[slot]->init( sched, sroute_player_of[slot], sroute_cnv_of[slot], sroute_lin_of[slot], true );
+	return 0;
+}
+
+
+static SQInteger sroute_append(HSQUIRRELVM vm) // slot, coord3d
+{
+	// what TOOL_SCHEDULE_ADD does while the editor is open: change the edited
+	// schedule in place. The component only notices it on its next draw.
+	gui_schedule_t *gui = sroute_slot(vm, 2);
+	grund_t *gr = welt->lookup( param<koord3d>::get(vm, 3) );
+	if(  gui == NULL  ||  gr == NULL  ) {
+		return sq_raise_error(vm, "sroute_append: empty slot or no ground");
+	}
+	gui->get_schedule()->append( gr, 0, 0 );
+	return 0;
+}
+
+
+static SQInteger sroute_draw(HSQUIRRELVM vm) // slot
+{
+	// One draw of the component: the path that notices a changed schedule.
+	// It needs a real clip rect, so this only has an effect with a graphics
+	// backend - the headless backend always reports an empty clip rect and
+	// gui_container_t::draw then skips every child.
+	gui_schedule_t *gui = sroute_slot(vm, 2);
+	if(  gui == NULL  ) {
+		return sq_raise_error(vm, "sroute_draw: empty slot");
+	}
+	const scr_size screen = gfx->get_screen_size();
+	gfx->set_clip_rect( 0, 0, screen.w, screen.h  CLIP_NUM_DEFAULT, false );
+	gui->set_size( scr_size(400, 400) );
+	gui->draw( scr_coord(0, 0) );
+	return 0;
+}
+
+
+static SQInteger sroute_close(HSQUIRRELVM vm) // slot
+{
+	const uint8 slot = param<uint8>::get(vm, 2);
+	if(  slot >= SROUTE_SLOTS  ) {
+		return sq_raise_error(vm, "sroute_close: bad slot");
+	}
+	delete sroute_gui[slot];
+	sroute_gui[slot] = NULL;
+	return 0;
+}
+
+
+static SQInteger sroute_step(HSQUIRRELVM)
+{
+	// what karte_t::interactive() does once per loop
+	welt->step_schedule_route();
+	return 0;
+}
+
+
+static SQInteger sroute_rotate(HSQUIRRELVM)
+{
+	// rotating rewrites every coordinate; the overlay must not survive it
+	welt->rotate90();
+	return 0;
+}
+
+
+static SQInteger sroute_len(HSQUIRRELVM vm)
+{
+	return param<uint32>::push( vm, welt->get_schedule_route().get_count() );
+}
+
+
+static SQInteger sroute_gaps(HSQUIRRELVM vm)
+{
+	uint32 gaps = 0;
+	for(  koord3d const& pos : welt->get_schedule_route()  ) {
+		gaps += (pos == koord3d::invalid);
+	}
+	return param<uint32>::push( vm, gaps );
+}
+
+
+static SQInteger sroute_tile(HSQUIRRELVM vm) // index -> coord3d, null for a gap
+{
+	const uint32 index = param<uint32>::get(vm, 2);
+	const vector_tpl<koord3d> &route = welt->get_schedule_route();
+	if(  index >= route.get_count()  ||  route[index] == koord3d::invalid  ) {
+		sq_pushnull(vm);
+		return 1;
+	}
+	return param<koord3d>::push( vm, route[index] );
+}
+
+
+static SQInteger sroute_has(HSQUIRRELVM vm) // coord3d -> bool
+{
+	const koord3d pos = param<koord3d>::get(vm, 2);
+	return param<bool>::push( vm, welt->get_schedule_route().is_contained(pos) );
+}
+
+
+static SQInteger sroute_owner(HSQUIRRELVM vm)
+{
+	return param<uint32>::push( vm, welt->get_schedule_route_owner() );
+}
+
+
+static SQInteger sroute_player(HSQUIRRELVM vm)
+{
+	return param<uint32>::push( vm, welt->get_schedule_route_player_nr() );
+}
+
+
+static SQInteger sroute_stop_marked(HSQUIRRELVM vm) // coord3d -> bool
+{
+	const koord3d pos = param<koord3d>::get(vm, 2);
+	bool marked = false;
+	if(  grund_t *gr = welt->lookup(pos)  ) {
+		marked = gr->get_flag( grund_t::marked );
+		for(  uint i = 0;  !marked  &&  i < gr->obj_count();  i++  ) {
+			marked = gr->obj_bei(i)->get_flag( obj_t::highlight );
+		}
+	}
+	return param<bool>::push( vm, marked );
+}
+
+
+static SQInteger sroute_view(HSQUIRRELVM vm) // coord3d - centre the map on a tile
+{
+	welt->get_viewport()->change_world_position( param<koord3d>::get(vm, 2) );
+	return 0;
+}
+
+
+static SQInteger sroute_shot(HSQUIRRELVM vm) // -> bool, writes simscrNN.png
+{
+	// same file naming as tool_screenshot_t, which since r12111 has to be done
+	// by the caller
+	static int number = 0;
+	char filename[80];
+	do {
+		sprintf( filename, SCREENSHOT_PATH_X "simscr%02d.png", number++ );
+	} while(  access( filename, W_OK ) != -1  );
+
+	const scr_rect screen_area = { { 0, 0 }, gfx->get_screen_size() };
+	return param<bool>::push( vm, gfx->take_screenshot( screen_area, filename ) );
+}
+
+
+static SQInteger sroute_convoy_fingerprint(HSQUIRRELVM vm) // convoy_x -> string
+{
+	convoihandle_t cnv = param<convoihandle_t>::get(vm, 2);
+	if(  !cnv.is_bound()  ) {
+		return sq_raise_error(vm, "sroute_convoy_fingerprint: no convoy");
+	}
+	cbuffer_t buf;
+	buf.printf( "%s|%d|%u|%d|%d",
+		cnv->get_pos().get_str(), (int)cnv->get_state(), cnv->get_route()->get_count(),
+		(int)cnv->get_akt_speed(), (int)cnv->get_schedule()->get_current_stop() );
+	return param<const char*>::push( vm, (const char*)buf );
+}
+
+
+void export_schedule_route_test(HSQUIRRELVM vm)
+{
+	register_function(vm, sroute_open,        "sroute_open",        4, ".ixi");
+	register_function(vm, sroute_open_line,   "sroute_open_line",   3, ".ix");
+	register_function(vm, sroute_open_convoy, "sroute_open_convoy", 3, ".ix");
+	register_function(vm, sroute_mark,        "sroute_mark",        3, ".ib");
+	register_function(vm, sroute_reinit,      "sroute_reinit",      3, ".ix");
+	register_function(vm, sroute_append,      "sroute_append",      3, ".ix");
+	register_function(vm, sroute_draw,        "sroute_draw",        2, ".i");
+	register_function(vm, sroute_close,       "sroute_close",       2, ".i");
+	register_function(vm, sroute_step,        "sroute_step",        1, ".");
+	register_function(vm, sroute_rotate,      "sroute_rotate",      1, ".");
+	register_function(vm, sroute_view,        "sroute_view",        2, ".x");
+	register_function(vm, sroute_shot,        "sroute_shot",        1, ".");
+	register_function(vm, sroute_len,         "sroute_len",         1, ".");
+	register_function(vm, sroute_gaps,        "sroute_gaps",        1, ".");
+	register_function(vm, sroute_tile,        "sroute_tile",        2, ".i");
+	register_function(vm, sroute_has,         "sroute_has",         2, ".x");
+	register_function(vm, sroute_owner,       "sroute_owner",       1, ".");
+	register_function(vm, sroute_player,      "sroute_player",      1, ".");
+	register_function(vm, sroute_stop_marked, "sroute_stop_marked", 2, ".x");
+	register_function(vm, sroute_convoy_fingerprint, "sroute_convoy_fingerprint", 2, ".x");
+}
diff --git a/tests/tests/test_schedule_route.nut b/tests/tests/test_schedule_route.nut
new file mode 100644
index 000000000..0ee7bf62b
--- /dev/null
+++ b/tests/tests/test_schedule_route.nut
@@ -0,0 +1,744 @@
+//
+// This file is part of the Simutrans project under the Artistic License.
+// (see LICENSE.txt)
+//
+
+
+//
+// Tests for the route overlay of the schedule editor.
+//
+// The sroute_* functions drive the real gui_schedule_t component and read the
+// real display only route channel of the world (api_schedule_route_test.cc),
+// so these tests exercise the production code path.
+//
+// Terminology used below:
+//   route      the ordered tiles the world stores for the shown schedule
+//   gap        a koord3d::invalid entry, marking a leg that has no route
+//   owner      the id of the editor component the route currently belongs to
+//   stop mark  the existing red highlight of the schedule stops
+//
+// Run the suite with -theme classic.tab. That is a precondition of these tests,
+// not a workaround: a headless build loads no theme by design, while these tests
+// are the only ones that build a real gui_schedule_t, and a schedule entry reads
+// skinverwaltung_t::gadget for its delete button. Without a theme the process dies
+// as the first test opens an editor - no assertion, no verdict, the log simply
+// stops. Whether that read should be guarded is a question about gui_schedule_t
+// and is discussed on its own, deliberately not settled by this patch.
+//
+
+
+function sroute_make_road(pl, from, to)
+{
+	local road = way_desc_x.get_available_ways(wt_road, st_flat)[0]
+	ASSERT_TRUE(road != null)
+	ASSERT_EQUAL(command_x.build_way(pl, from, to, road, true), null)
+}
+
+
+function sroute_remove_way(pl, from, to, wt = wt_road)
+{
+	ASSERT_EQUAL(command_x(tool_remove_way).work(pl, from, to, "" + wt), null)
+}
+
+
+// create_line does not hand back the line it made, so find the new one by name
+function sroute_new_line(pl, wt)
+{
+	local before = {}
+	foreach (l in pl.get_line_list()) {
+		before[l.get_name()] <- true
+	}
+
+	pl.create_line(wt)
+
+	local found = null
+	foreach (l in pl.get_line_list()) {
+		if (!(l.get_name() in before)) {
+			found = l
+		}
+	}
+	ASSERT_TRUE(found != null)
+	return found
+}
+
+
+function sroute_schedule(wt, positions)
+{
+	local entries = []
+	foreach (pos in positions) {
+		entries.append(schedule_entry_x(pos, 0, 0))
+	}
+	return schedule_x(wt, entries)
+}
+
+
+// every route tile must carry a way of that type: the geometry follows the
+// infrastructure and is not a straight line between the stops
+function ASSERT_ROUTE_ON_WAY(wt)
+{
+	for (local i = 0; i < sroute_len(); ++i) {
+		local pos = sroute_tile(i)
+		if (pos == null) {
+			continue // gap
+		}
+		local tile = square_x(pos.x, pos.y).get_tile_at_height(pos.z)
+		ASSERT_TRUE(tile != null)
+		ASSERT_TRUE(tile.has_way(wt))
+	}
+}
+
+
+//
+// 1 + 2 + 15: opening shows stops and route, closing removes both,
+//             and the geometry follows the way around a corner
+//
+function test_schedule_route_open_and_close()
+{
+	local pl = player_x(0)
+	sroute_make_road(pl, coord3d(2, 2, 0), coord3d(2, 8, 0))
+	sroute_make_road(pl, coord3d(2, 8, 0), coord3d(8, 8, 0))
+
+	local sched = sroute_schedule(wt_road, [coord3d(2, 2, 0), coord3d(8, 8, 0)])
+
+	sroute_open(0, sched, 0)
+	sroute_mark(0, true)
+
+	// nothing is installed before a safe step: the calculation never runs from the GUI
+	ASSERT_EQUAL(sroute_len(), 0)
+	ASSERT_TRUE(sroute_owner() != 0)
+
+	sroute_step()
+
+	ASSERT_TRUE(sroute_len() > 0)
+	ASSERT_EQUAL(sroute_gaps(), 0)
+	ASSERT_EQUAL(sroute_player(), 0)
+	ASSERT_ROUTE_ON_WAY(wt_road)
+
+	// the real path, not a straight line: both legs of the L are on it
+	ASSERT_TRUE(sroute_has(coord3d(2, 5, 0)))
+	ASSERT_TRUE(sroute_has(coord3d(2, 8, 0)))
+	ASSERT_TRUE(sroute_has(coord3d(5, 8, 0)))
+
+	// two independent channels: a tile can be on the route without being a stop
+	ASSERT_TRUE(sroute_stop_marked(coord3d(2, 2, 0)))
+	ASSERT_TRUE(sroute_stop_marked(coord3d(8, 8, 0)))
+	ASSERT_FALSE(sroute_stop_marked(coord3d(2, 5, 0)))
+
+	// closing the editor drops both
+	sroute_close(0)
+	ASSERT_EQUAL(sroute_len(), 0)
+	ASSERT_EQUAL(sroute_owner(), 0)
+	ASSERT_FALSE(sroute_stop_marked(coord3d(2, 2, 0)))
+
+	sroute_remove_way(pl, coord3d(2, 2, 0), coord3d(2, 8, 0))
+	sroute_remove_way(pl, coord3d(2, 8, 0), coord3d(8, 8, 0))
+	ASSERT_FALSE(tile_x(5, 8, 0).has_way(wt_road))
+	RESET_ALL_PLAYER_FUNDS()
+}
+
+
+//
+// 3: changing the schedule invalidates the old route and produces a new one
+//
+function test_schedule_route_update_on_change()
+{
+	local pl = player_x(0)
+	sroute_make_road(pl, coord3d(2, 2, 0), coord3d(2, 12, 0))
+
+	local sched = sroute_schedule(wt_road, [coord3d(2, 2, 0), coord3d(2, 6, 0)])
+	sroute_open(0, sched, 0)
+	sroute_mark(0, true)
+	sroute_step()
+
+	local short_len = sroute_len()
+	ASSERT_TRUE(short_len > 0)
+	ASSERT_FALSE(sroute_has(coord3d(2, 10, 0)))
+
+	// the schedule gained a stop; the editor is re-initialised with it, as
+	// line_management_gui does when the schedule of the shown line changed
+	local longer = sroute_schedule(wt_road, [coord3d(2, 2, 0), coord3d(2, 12, 0)])
+	sroute_reinit(0, longer)
+	sroute_mark(0, true)
+
+	// the old route is dropped at once, the new one only after a safe step
+	ASSERT_EQUAL(sroute_len(), 0)
+	sroute_step()
+
+	ASSERT_TRUE(sroute_len() > short_len)
+	ASSERT_TRUE(sroute_has(coord3d(2, 10, 0)))
+	ASSERT_ROUTE_ON_WAY(wt_road)
+
+	sroute_close(0)
+	sroute_remove_way(pl, coord3d(2, 2, 0), coord3d(2, 12, 0))
+	RESET_ALL_PLAYER_FUNDS()
+}
+
+
+//
+// 4: a second editor replaces the first one, and closing the first
+//    does not remove the route of the second
+//
+function test_schedule_route_second_editor()
+{
+	local pl = player_x(0)
+	sroute_make_road(pl, coord3d(2, 2, 0), coord3d(2, 12, 0))
+
+	local short_sched = sroute_schedule(wt_road, [coord3d(2, 2, 0), coord3d(2, 4, 0)])
+	local long_sched  = sroute_schedule(wt_road, [coord3d(2, 2, 0), coord3d(2, 12, 0)])
+
+	sroute_open(0, short_sched, 0)
+	sroute_mark(0, true)
+	sroute_step()
+	local first_owner = sroute_owner()
+	local first_len = sroute_len()
+	ASSERT_TRUE(first_len > 0)
+
+	sroute_open(1, long_sched, 0)
+	sroute_mark(1, true)
+	sroute_step()
+	local second_owner = sroute_owner()
+
+	ASSERT_TRUE(second_owner != first_owner)
+	ASSERT_TRUE(sroute_len() > first_len)
+	ASSERT_TRUE(sroute_has(coord3d(2, 10, 0)))
+
+	// closing the older window must not touch the newer route
+	sroute_close(0)
+	ASSERT_EQUAL(sroute_owner(), second_owner)
+	ASSERT_TRUE(sroute_has(coord3d(2, 10, 0)))
+
+	sroute_close(1)
+	ASSERT_EQUAL(sroute_len(), 0)
+	ASSERT_EQUAL(sroute_owner(), 0)
+
+	sroute_remove_way(pl, coord3d(2, 2, 0), coord3d(2, 12, 0))
+	RESET_ALL_PLAYER_FUNDS()
+}
+
+
+//
+// 5: an outdated request may never overwrite a newer visualization
+//
+function test_schedule_route_outdated_request()
+{
+	local pl = player_x(0)
+	sroute_make_road(pl, coord3d(2, 2, 0), coord3d(2, 12, 0))
+
+	local a = sroute_schedule(wt_road, [coord3d(2, 2, 0), coord3d(2, 4, 0)])
+	local b = sroute_schedule(wt_road, [coord3d(2, 2, 0), coord3d(2, 12, 0)])
+
+	// a request that is never stepped, then the window closes
+	sroute_open(0, a, 0)
+	sroute_mark(0, true)
+	sroute_close(0)
+	sroute_step()
+	ASSERT_EQUAL(sroute_len(), 0)
+	ASSERT_EQUAL(sroute_owner(), 0)
+
+	// an older window closes while a newer request is still pending
+	sroute_open(0, a, 0)
+	sroute_mark(0, true)
+	sroute_open(1, b, 0)
+	sroute_mark(1, true)
+	local newer = sroute_owner()
+	sroute_close(0)
+	sroute_step()
+
+	ASSERT_EQUAL(sroute_owner(), newer)
+	ASSERT_TRUE(sroute_has(coord3d(2, 10, 0)))
+
+	sroute_close(1)
+	sroute_remove_way(pl, coord3d(2, 2, 0), coord3d(2, 12, 0))
+	RESET_ALL_PLAYER_FUNDS()
+}
+
+
+//
+// 14: a leg without a route leaves a gap and never draws a false connection
+//
+function test_schedule_route_broken_leg()
+{
+	local pl = player_x(0)
+	sroute_make_road(pl, coord3d(2, 2, 0), coord3d(2, 5, 0))
+	sroute_make_road(pl, coord3d(9, 2, 0), coord3d(9, 5, 0))
+
+	// nothing at all can be routed: no geometry, and above all no false line
+	local no_leg = sroute_schedule(wt_road, [coord3d(2, 2, 0), coord3d(9, 2, 0)])
+	sroute_open(0, no_leg, 0)
+	sroute_mark(0, true)
+	sroute_step()
+	ASSERT_EQUAL(sroute_len(), 0)
+	ASSERT_FALSE(sroute_has(coord3d(5, 2, 0)))
+
+	// but the stops are still marked: the two channels are independent
+	ASSERT_TRUE(sroute_stop_marked(coord3d(2, 2, 0)))
+	ASSERT_TRUE(sroute_stop_marked(coord3d(9, 2, 0)))
+	sroute_close(0)
+
+	// one leg routes and the next one does not: the good leg is kept and the
+	// broken one becomes an explicit gap the display never draws across
+	local one_leg = sroute_schedule(wt_road, [coord3d(2, 2, 0), coord3d(2, 5, 0), coord3d(9, 2, 0)])
+	sroute_open(0, one_leg, 0)
+	sroute_mark(0, true)
+	sroute_step()
+
+	ASSERT_EQUAL(sroute_gaps(), 1)
+	ASSERT_TRUE(sroute_has(coord3d(2, 3, 0)))
+	ASSERT_FALSE(sroute_has(coord3d(5, 2, 0)))
+	ASSERT_EQUAL(sroute_tile(sroute_len() - 1), null) // the gap closes the list
+	ASSERT_ROUTE_ON_WAY(wt_road)
+
+	sroute_close(0)
+	sroute_remove_way(pl, coord3d(2, 2, 0), coord3d(2, 5, 0))
+	sroute_remove_way(pl, coord3d(9, 2, 0), coord3d(9, 5, 0))
+	RESET_ALL_PLAYER_FUNDS()
+}
+
+
+//
+// 11: rail
+//
+function test_schedule_route_rail()
+{
+	local pl = player_x(0)
+	local rail = way_desc_x.get_available_ways(wt_rail, st_flat)[0]
+	ASSERT_TRUE(rail != null)
+	ASSERT_EQUAL(command_x.build_way(pl, coord3d(3, 3, 0), coord3d(3, 11, 0), rail, true), null)
+
+	local sched = sroute_schedule(wt_rail, [coord3d(3, 3, 0), coord3d(3, 11, 0)])
+	sroute_open(0, sched, 0)
+	sroute_mark(0, true)
+	sroute_step()
+
+	ASSERT_TRUE(sroute_len() > 0)
+	ASSERT_EQUAL(sroute_gaps(), 0)
+	ASSERT_TRUE(sroute_has(coord3d(3, 7, 0)))
+	ASSERT_ROUTE_ON_WAY(wt_rail)
+
+	sroute_close(0)
+	sroute_remove_way(pl, coord3d(3, 3, 0), coord3d(3, 11, 0), wt_rail)
+	RESET_ALL_PLAYER_FUNDS()
+}
+
+
+//
+// 12: monorail. pak64 offers no maglev way at the start date of the test map,
+//     so maglev cannot be covered here; it shares this code path.
+//
+function test_schedule_route_monorail()
+{
+	local pl = player_x(0)
+
+	foreach (wt in [wt_monorail]) {
+		local ways = way_desc_x.get_available_ways(wt, st_flat)
+		ASSERT_TRUE(ways.len() > 0)
+		local way = ways[0]
+
+		ASSERT_EQUAL(command_x.build_way(pl, coord3d(6, 3, 0), coord3d(6, 9, 0), way, true), null)
+
+		local sched = sroute_schedule(wt, [coord3d(6, 3, 0), coord3d(6, 9, 0)])
+		sroute_open(0, sched, 0)
+		sroute_mark(0, true)
+		sroute_step()
+
+		ASSERT_TRUE(sroute_len() > 0)
+		ASSERT_EQUAL(sroute_gaps(), 0)
+		ASSERT_TRUE(sroute_has(coord3d(6, 6, 0)))
+		ASSERT_ROUTE_ON_WAY(wt)
+
+		sroute_close(0)
+		sroute_remove_way(pl, coord3d(6, 3, 0), coord3d(6, 9, 0), wt)
+	}
+	RESET_ALL_PLAYER_FUNDS()
+}
+
+
+//
+// 13: ships
+//
+function test_schedule_route_water()
+{
+	local pl = player_x(0)
+	local climate = command_x(tool_set_climate)
+
+	ASSERT_EQUAL(climate.work(pl, coord3d(10, 2, 0), coord3d(12, 10, 0), "" + cl_water), null)
+
+	local sched = sroute_schedule(wt_water, [coord3d(10, 2, 0), coord3d(12, 10, 0)])
+	sroute_open(0, sched, 0)
+	sroute_mark(0, true)
+	sroute_step()
+
+	ASSERT_TRUE(sroute_len() > 0)
+	ASSERT_EQUAL(sroute_gaps(), 0)
+
+	sroute_close(0)
+	ASSERT_EQUAL(climate.work(pl, coord3d(10, 2, 0), coord3d(12, 10, 0), "" + cl_mediterran), null)
+	RESET_ALL_PLAYER_FUNDS()
+}
+
+
+//
+// 8: a line without convois still gets a real route, using a generic vehicle
+//
+function test_schedule_route_line_without_convoys()
+{
+	local pl = player_x(0)
+	sroute_make_road(pl, coord3d(2, 2, 0), coord3d(2, 10, 0))
+
+	local line = sroute_new_line(pl, wt_road)
+	ASSERT_EQUAL(line.get_convoy_list().get_count(), 0)
+
+	local sched = sroute_schedule(wt_road, [coord3d(2, 2, 0), coord3d(2, 10, 0)])
+	ASSERT_TRUE(line.change_schedule(pl, sched))
+
+	sroute_open_line(0, line)
+	sroute_mark(0, true)
+	sroute_step()
+
+	ASSERT_TRUE(sroute_len() > 0)
+	ASSERT_EQUAL(sroute_gaps(), 0)
+	ASSERT_EQUAL(sroute_player(), 0)
+	ASSERT_TRUE(sroute_has(coord3d(2, 6, 0)))
+	ASSERT_ROUTE_ON_WAY(wt_road)
+
+	sroute_close(0)
+	line.destroy(pl)
+	sroute_remove_way(pl, coord3d(2, 2, 0), coord3d(2, 10, 0))
+	RESET_ALL_PLAYER_FUNDS()
+}
+
+
+//
+// 6 + 7: a real convoi, and a line that has one. The calculation must not
+//        touch the convoi.
+//
+function test_schedule_route_convoy_and_line()
+{
+	local pl = player_x(0)
+	sroute_make_road(pl, coord3d(2, 2, 0), coord3d(2, 10, 0))
+	ASSERT_EQUAL(command_x.build_depot(pl, coord3d(2, 2, 0), get_depot_by_wt(wt_road)), null)
+
+	local the_depot = depot_x.get_depot_list(pl, wt_road)[0]
+	local vehicle = vehicle_desc_x.get_available_vehicles(wt_road)[0]
+	ASSERT_TRUE(vehicle != null)
+	ASSERT_TRUE(the_depot.append_vehicle(pl, convoy_x(0), vehicle))
+	local cnv = the_depot.get_convoy_list()[0]
+
+	local sched = sroute_schedule(wt_road, [coord3d(2, 4, 0), coord3d(2, 10, 0)])
+	ASSERT_TRUE(cnv.change_schedule(pl, sched))
+
+	// 6: the convoi is not modified by the calculation
+	local before = sroute_convoy_fingerprint(cnv)
+	sroute_open_convoy(0, cnv)
+	sroute_mark(0, true)
+	sroute_step()
+
+	ASSERT_TRUE(sroute_len() > 0)
+	ASSERT_EQUAL(sroute_gaps(), 0)
+	ASSERT_EQUAL(sroute_player(), 0)
+	ASSERT_ROUTE_ON_WAY(wt_road)
+	ASSERT_EQUAL(sroute_convoy_fingerprint(cnv), before)
+	sroute_close(0)
+
+	// 7: the same schedule through a line that has this convoi
+	local line = sroute_new_line(pl, wt_road)
+	ASSERT_TRUE(line.change_schedule(pl, sched))
+	ASSERT_TRUE(cnv.set_line(pl, line))
+	ASSERT_EQUAL(line.get_convoy_list().get_count(), 1)
+
+	sroute_open_line(0, line)
+	sroute_mark(0, true)
+	sroute_step()
+
+	ASSERT_TRUE(sroute_len() > 0)
+	ASSERT_EQUAL(sroute_gaps(), 0)
+	ASSERT_ROUTE_ON_WAY(wt_road)
+	ASSERT_EQUAL(sroute_convoy_fingerprint(cnv), before)
+
+	sroute_close(0)
+	ASSERT_TRUE(cnv.destroy(pl))
+	line.destroy(pl)
+	ASSERT_EQUAL(command_x(tool_remover).work(pl, coord3d(2, 2, 0)), null)
+	sroute_remove_way(pl, coord3d(2, 2, 0), coord3d(2, 10, 0))
+	RESET_ALL_PLAYER_FUNDS()
+}
+
+
+//
+// 9: the owner decides the route - a private way blocks another player
+//
+function test_schedule_route_private_way()
+{
+	local pl = player_x(0)
+	local public_pl = player_x(1)
+	local sign = sign_desc_x.get_available_signs(wt_road).filter(@(idx, sign) sign.is_private_way())[0]
+	ASSERT_TRUE(sign != null)
+
+	sroute_make_road(public_pl, coord3d(2, 2, 0), coord3d(2, 10, 0))
+	ASSERT_EQUAL(command_x.build_sign_at(pl, coord3d(2, 6, 0), sign), null)
+
+	local sched = sroute_schedule(wt_road, [coord3d(2, 2, 0), coord3d(2, 10, 0)])
+
+	// the owner of the private way gets through
+	sroute_open(0, sched, 0)
+	sroute_mark(0, true)
+	sroute_step()
+	ASSERT_TRUE(sroute_has(coord3d(2, 6, 0)))
+	ASSERT_EQUAL(sroute_gaps(), 0)
+	sroute_close(0)
+
+	// another player does not
+	sroute_open(0, sched, 1)
+	sroute_mark(0, true)
+	sroute_step()
+	ASSERT_EQUAL(sroute_player(), 1)
+	ASSERT_FALSE(sroute_has(coord3d(2, 6, 0)))
+	ASSERT_EQUAL(sroute_len(), 0) // the only way through is closed for them
+	sroute_close(0)
+
+	ASSERT_EQUAL(command_x(tool_remover).work(pl, coord3d(2, 6, 0)), null)
+	sroute_remove_way(public_pl, coord3d(2, 2, 0), coord3d(2, 10, 0))
+	RESET_ALL_PLAYER_FUNDS()
+}
+
+
+//
+// 16: rotating the map must not leave residual geometry
+//
+function test_schedule_route_rotate()
+{
+	local pl = player_x(0)
+	sroute_make_road(pl, coord3d(2, 2, 0), coord3d(2, 10, 0))
+
+	local sched = sroute_schedule(wt_road, [coord3d(2, 2, 0), coord3d(2, 10, 0)])
+	sroute_open(0, sched, 0)
+	sroute_mark(0, true)
+	sroute_step()
+	ASSERT_TRUE(sroute_len() > 0)
+
+	sroute_rotate()
+	ASSERT_EQUAL(sroute_len(), 0)
+	ASSERT_EQUAL(sroute_owner(), 0)
+
+	// back to the original orientation, so the rest of the suite is unaffected
+	sroute_rotate()
+	sroute_rotate()
+	sroute_rotate()
+	ASSERT_EQUAL(sroute_len(), 0)
+
+	sroute_close(0)
+	sroute_remove_way(pl, coord3d(2, 2, 0), coord3d(2, 10, 0))
+	RESET_ALL_PLAYER_FUNDS()
+}
+
+
+//
+// Catenary cases below share one map: a direct corridor at x=4 between (4,3) and
+// (4,11), and a longer way round through x=8. Only the way round carries overhead
+// line, junctions included, so the two corridors differ in exactly one property.
+// The depot hangs off a spur at (2,3) and is never on any of the routes.
+//
+function sroute_build_catenary_loop(pl)
+{
+	local rail = way_desc_x.get_available_ways(wt_rail, st_flat)[0]
+	ASSERT_TRUE(rail != null)
+	ASSERT_EQUAL(command_x.build_way(pl, coord3d(4, 3, 0), coord3d(4, 11, 0), rail, true), null)
+	ASSERT_EQUAL(command_x.build_way(pl, coord3d(4, 3, 0), coord3d(8, 3, 0), rail, true), null)
+	ASSERT_EQUAL(command_x.build_way(pl, coord3d(8, 3, 0), coord3d(8, 11, 0), rail, true), null)
+	ASSERT_EQUAL(command_x.build_way(pl, coord3d(8, 11, 0), coord3d(4, 11, 0), rail, true), null)
+	ASSERT_EQUAL(command_x.build_way(pl, coord3d(2, 3, 0), coord3d(4, 3, 0), rail, true), null)
+
+	local ohl = wayobj_desc_x.get_available_wayobjs(wt_rail).filter(@(idx, w) w.is_overhead_line())[0]
+	ASSERT_TRUE(ohl != null)
+	ASSERT_EQUAL(command_x.build_wayobj(pl, coord3d(4, 3, 0), coord3d(8, 3, 0), ohl), null)
+	ASSERT_EQUAL(command_x.build_wayobj(pl, coord3d(8, 3, 0), coord3d(8, 11, 0), ohl), null)
+	ASSERT_EQUAL(command_x.build_wayobj(pl, coord3d(8, 11, 0), coord3d(4, 11, 0), ohl), null)
+
+	// the point of the whole layout: the short way is the one without catenary
+	ASSERT_TRUE(tile_x(8, 7, 0).find_object(mo_wayobj) != null)
+	ASSERT_EQUAL(tile_x(4, 7, 0).find_object(mo_wayobj), null)
+
+	ASSERT_EQUAL(command_x.build_depot(pl, coord3d(2, 3, 0), get_depot_by_wt(wt_rail)), null)
+	return depot_x.get_depot_list(pl, wt_rail)[0]
+}
+
+
+function sroute_clear_catenary_loop(pl)
+{
+	local wrem = command_x(tool_remove_wayobj)
+	ASSERT_EQUAL(wrem.work(pl, coord3d(4, 3, 0), coord3d(8, 3, 0), "" + wt_rail), null)
+	ASSERT_EQUAL(wrem.work(pl, coord3d(8, 3, 0), coord3d(8, 11, 0), "" + wt_rail), null)
+	ASSERT_EQUAL(wrem.work(pl, coord3d(8, 11, 0), coord3d(4, 11, 0), "" + wt_rail), null)
+	ASSERT_EQUAL(command_x(tool_remover).work(pl, coord3d(2, 3, 0)), null)
+	sroute_remove_way(pl, coord3d(4, 3, 0), coord3d(4, 11, 0), wt_rail)
+	sroute_remove_way(pl, coord3d(4, 3, 0), coord3d(8, 3, 0), wt_rail)
+	sroute_remove_way(pl, coord3d(8, 3, 0), coord3d(8, 11, 0), wt_rail)
+	sroute_remove_way(pl, coord3d(8, 11, 0), coord3d(4, 11, 0), wt_rail)
+	sroute_remove_way(pl, coord3d(2, 3, 0), coord3d(4, 3, 0), wt_rail)
+	RESET_ALL_PLAYER_FUNDS()
+}
+
+
+function sroute_electric_loco()
+{
+	local list = vehicle_desc_x.get_available_vehicles(wt_rail).filter(@(idx, v) (v.needs_electrification() && v.can_be_first()))
+	ASSERT_TRUE(list.len() > 0)
+	return list[0]
+}
+
+
+function sroute_diesel_loco()
+{
+	local list = vehicle_desc_x.get_available_vehicles(wt_rail).filter(@(idx, v) (!v.needs_electrification() && v.can_be_first() && v.get_power() > 0))
+	ASSERT_TRUE(list.len() > 0)
+	return list[0]
+}
+
+
+//
+// 18: an electric convoi is shown the route it could actually take, so it goes
+//     the long way round rather than over the unelectrified short one
+//
+function test_schedule_route_electric_follows_catenary()
+{
+	local pl = player_x(0)
+	local the_depot = sroute_build_catenary_loop(pl)
+
+	ASSERT_TRUE(the_depot.append_vehicle(pl, convoy_x(0), sroute_electric_loco()))
+	local cnv = the_depot.get_convoy_list()[0]
+	ASSERT_TRUE(cnv.needs_electrification())
+
+	local sched = sroute_schedule(wt_rail, [coord3d(4, 3, 0), coord3d(4, 11, 0)])
+	ASSERT_TRUE(cnv.change_schedule(pl, sched))
+
+	sroute_open_convoy(0, cnv)
+	sroute_mark(0, true)
+	sroute_step()
+
+	ASSERT_TRUE(sroute_len() > 0)
+	ASSERT_EQUAL(sroute_gaps(), 0)
+	ASSERT_TRUE(sroute_has(coord3d(8, 7, 0)))
+	ASSERT_FALSE(sroute_has(coord3d(4, 7, 0)))
+	ASSERT_ROUTE_ON_WAY(wt_rail)
+
+	sroute_close(0)
+	ASSERT_TRUE(cnv.destroy(pl))
+	sroute_clear_catenary_loop(pl)
+}
+
+
+//
+// 19: the restriction comes from the convoi, not from the way: on the same map a
+//     convoi that does not need catenary takes the short corridor
+//
+function test_schedule_route_diesel_ignores_catenary()
+{
+	local pl = player_x(0)
+	local the_depot = sroute_build_catenary_loop(pl)
+
+	ASSERT_TRUE(the_depot.append_vehicle(pl, convoy_x(0), sroute_diesel_loco()))
+	local cnv = the_depot.get_convoy_list()[0]
+	ASSERT_FALSE(cnv.needs_electrification())
+
+	local sched = sroute_schedule(wt_rail, [coord3d(4, 3, 0), coord3d(4, 11, 0)])
+	ASSERT_TRUE(cnv.change_schedule(pl, sched))
+
+	sroute_open_convoy(0, cnv)
+	sroute_mark(0, true)
+	sroute_step()
+
+	ASSERT_TRUE(sroute_len() > 0)
+	ASSERT_EQUAL(sroute_gaps(), 0)
+	ASSERT_TRUE(sroute_has(coord3d(4, 7, 0)))
+	ASSERT_FALSE(sroute_has(coord3d(8, 7, 0)))
+	ASSERT_ROUTE_ON_WAY(wt_rail)
+
+	sroute_close(0)
+	ASSERT_TRUE(cnv.destroy(pl))
+	sroute_clear_catenary_loop(pl)
+}
+
+
+//
+// 20: with the catenary gone there is no route at all for an electric convoi, and
+//     an empty overlay is not the same as no overlay - the editor still owns it
+//
+function test_schedule_route_electric_without_catenary()
+{
+	local pl = player_x(0)
+	local rail = way_desc_x.get_available_ways(wt_rail, st_flat)[0]
+	ASSERT_EQUAL(command_x.build_way(pl, coord3d(3, 3, 0), coord3d(3, 11, 0), rail, true), null)
+	ASSERT_EQUAL(command_x.build_depot(pl, coord3d(3, 3, 0), get_depot_by_wt(wt_rail)), null)
+	local the_depot = depot_x.get_depot_list(pl, wt_rail)[0]
+
+	local sched = sroute_schedule(wt_rail, [coord3d(3, 5, 0), coord3d(3, 11, 0)])
+
+	ASSERT_TRUE(the_depot.append_vehicle(pl, convoy_x(0), sroute_electric_loco()))
+	local cnv = the_depot.get_convoy_list()[0]
+	ASSERT_TRUE(cnv.change_schedule(pl, sched))
+	sroute_open_convoy(0, cnv)
+	sroute_mark(0, true)
+	sroute_step()
+
+	ASSERT_EQUAL(sroute_len(), 0)
+	ASSERT_TRUE(sroute_owner() != 0)
+	sroute_close(0)
+	ASSERT_TRUE(cnv.destroy(pl))
+
+	// the control: the very same schedule over the very same track does have a
+	// route, so the empty one above is the electrification and nothing else
+	ASSERT_TRUE(the_depot.append_vehicle(pl, convoy_x(0), sroute_diesel_loco()))
+	local cnv2 = the_depot.get_convoy_list()[0]
+	ASSERT_TRUE(cnv2.change_schedule(pl, sched))
+	sroute_open_convoy(0, cnv2)
+	sroute_mark(0, true)
+	sroute_step()
+
+	ASSERT_TRUE(sroute_len() > 0)
+	ASSERT_EQUAL(sroute_gaps(), 0)
+	sroute_close(0)
+	ASSERT_TRUE(cnv2.destroy(pl))
+
+	ASSERT_EQUAL(command_x(tool_remover).work(pl, coord3d(3, 3, 0)), null)
+	sroute_remove_way(pl, coord3d(3, 3, 0), coord3d(3, 11, 0), wt_rail)
+	RESET_ALL_PLAYER_FUNDS()
+}
+
+
+//
+// 21: the restriction survives a schedule change - the editor is re-initialised
+//     with the same convoi, so the new route avoids the same tiles as the old one
+//
+function test_schedule_route_electric_survives_schedule_change()
+{
+	local pl = player_x(0)
+	local the_depot = sroute_build_catenary_loop(pl)
+
+	ASSERT_TRUE(the_depot.append_vehicle(pl, convoy_x(0), sroute_electric_loco()))
+	local cnv = the_depot.get_convoy_list()[0]
+
+	local sched = sroute_schedule(wt_rail, [coord3d(4, 3, 0), coord3d(4, 11, 0)])
+	ASSERT_TRUE(cnv.change_schedule(pl, sched))
+	sroute_open_convoy(0, cnv)
+	sroute_mark(0, true)
+	sroute_step()
+	ASSERT_TRUE(sroute_len() > 0)
+	ASSERT_FALSE(sroute_has(coord3d(4, 7, 0)))
+
+	// a stop is added in the middle of the electrified side
+	local longer = sroute_schedule(wt_rail, [coord3d(4, 3, 0), coord3d(8, 7, 0), coord3d(4, 11, 0)])
+	sroute_reinit(0, longer)
+	sroute_mark(0, true)
+	ASSERT_EQUAL(sroute_len(), 0)
+	sroute_step()
+
+	ASSERT_TRUE(sroute_len() > 0)
+	ASSERT_EQUAL(sroute_gaps(), 0)
+	ASSERT_TRUE(sroute_has(coord3d(8, 7, 0)))
+	ASSERT_FALSE(sroute_has(coord3d(4, 7, 0)))
+	ASSERT_ROUTE_ON_WAY(wt_rail)
+
+	sroute_close(0)
+	ASSERT_TRUE(cnv.destroy(pl))
+	sroute_clear_catenary_loop(pl)
+}
