diff --git a/changelog.txt b/changelog.txt
index f7e41acd3b..2b23a2c5b8 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -28,7 +28,10 @@ Template for new versions:
 
 ## New Tools
 - `sync-windmills`: synchronize or randomize movement of active windmills
-- `trackstop`: new overlay to allow changing track stop dump direction and friction and roller direction and speed after construction
+- `trackstop`: provides 3 new overlays:
+    - trackstop: allow changing track stop dump direction and friction
+    - rollers: allow changing roller direction and speed
+    - reorderstops: reorder stops in hauling routes
 
 ## New Features
 - `gui/design`: show selected dimensions next to the mouse cursor when designating with vanilla tools, for example when painting a burrow or designating digging
diff --git a/docs/trackstop.rst b/docs/trackstop.rst
index 88579b783f..50a2e0b685 100644
--- a/docs/trackstop.rst
+++ b/docs/trackstop.rst
@@ -5,6 +5,7 @@ trackstop
     :summary: Add dynamic configuration options for track stops.
     :tags: fort buildings interface
 
-This script provides 2 overlays that are managed by the `overlay` framework. The script does nothing when executed.
+This script provides 3 overlays that are managed by the `overlay` framework. The script does nothing when executed.
 The trackstop overlay allows the player to change the friction and dump direction of a selected track stop after it has been constructed.
 The rollers overlay allows the player to change the roller direction and speed of a selected roller after it has been constructed.
+The reorderstops overlay allows the player to change the order of stops in a hauling route.
diff --git a/trackstop.lua b/trackstop.lua
index 6657209b08..25f538d3f0 100644
--- a/trackstop.lua
+++ b/trackstop.lua
@@ -52,6 +52,31 @@ local DIRECTION_MAP = {
 
 local DIRECTION_MAP_REVERSE = utils.invert(DIRECTION_MAP)
 
+--[[
+  - swap 2 elements between different indexes in the same table like:
+    swap_elements({1, 2, 3}, 1, nil, 3) => {3, 2, 1}
+  - swap 2 elements at the specified indexes between 2 tables like:
+    swap_elements({1, 2, 3}, 1, {4, 5, 6}, 3) => {6, 2, 3} {4, 5, 1}
+]]--
+local function swap_elements(tbl1, index1, tbl2, index2)
+  tbl2 = tbl2 or tbl1
+  index2 = index2 or index1
+  tbl1[index1], tbl2[index2] = tbl2[index2], tbl1[index1]
+  return tbl1, tbl2
+end
+
+local function reset_guide_paths(conditions)
+  for _, condition in ipairs(conditions) do
+    local gpath = condition.guide_path
+
+    if gpath then
+      gpath.x:resize(0)
+      gpath.y:resize(0)
+      gpath.z:resize(0)
+    end
+  end
+end
+
 TrackStopOverlay = defclass(TrackStopOverlay, overlay.OverlayWidget)
 TrackStopOverlay.ATTRS{
   default_pos={x=-73, y=29},
@@ -120,7 +145,11 @@ function TrackStopOverlay:setDumpDirection(direction)
 end
 
 function TrackStopOverlay:render(dc)
-  local building = dfhack.gui.getSelectedBuilding()
+  local building = dfhack.gui.getSelectedBuilding(true)
+  if not building then
+    return
+  end
+
   local friction = building.friction
   local friction_cycle = self.subviews.friction
 
@@ -201,7 +230,10 @@ function RollerOverlay:setSpeed(speed)
 end
 
 function RollerOverlay:render(dc)
-  local building = dfhack.gui.getSelectedBuilding()
+  local building = dfhack.gui.getSelectedBuilding(true)
+  if not building then
+    return
+  end
 
   self.subviews.direction:setOption(DIRECTION_MAP_REVERSE[building.direction])
   self.subviews.speed:setOption(SPEED_MAP_REVERSE[building.speed])
@@ -236,7 +268,225 @@ function RollerOverlay:init()
   }
 end
 
+ReorderStopsWindow = defclass(ReorderStopsWindow, widgets.Window)
+ReorderStopsWindow.ATTRS {
+  frame={t=4,l=60,w=49, h=26},
+  frame_title='Reorder Stops',
+  resizable=true,
+}
+
+local SELECT_STOP_HINT = 'Select a stop to move'
+local SELECT_ANOTHER_STOP_HINT = 'Select another stop to swap or same to cancel'
+
+
+function ReorderStopsWindow:handleStopSelection(index, item)
+  -- Skip routes
+  if item.type == 'route' then return end
+
+  -- Select stop if none selected
+  if not self.first_selected_stop then
+    self:toggleStopSelection(item)
+    return
+  end
+
+  -- Swap stops
+  self:swapStops(index, item)
+
+  -- Reset stop properties
+  self:resetStopProperties(item)
+
+  self.first_selected_stop = nil
+  self:updateList()
+end
+
+function ReorderStopsWindow:toggleStopSelection(item)
+  if not self.first_selected_stop then
+    self.first_selected_stop = item
+  else
+    self.first_selected_stop = nil
+  end
+
+  self:updateList()
+end
+
+function ReorderStopsWindow:swapStops(index, second_selected_stop)
+  local hauling = df.global.plotinfo.hauling
+  local routes = hauling.routes
+  local view_stops = hauling.view_stops
+  local second_selected_stop_route = routes[second_selected_stop.route_index]
+  local second_selected_stop_index = second_selected_stop.stop_index
+  local same_route = self.first_selected_stop.route_index == second_selected_stop.route_index
+
+  if same_route then
+    swap_elements(second_selected_stop_route.stops, second_selected_stop_index, nil, self.first_selected_stop.stop_index)
+
+    -- find out what index the vehicle is currently at for this route, if there is one
+    local vehicle_index = nil
+    local hauling_route = df.hauling_route.get_vector()[second_selected_stop.route_index]
+
+    -- this vector will have 0 elements if there is no vehicle or 1 element if there is a vehicle
+    -- the element will be the index of the vehicle stop
+    for _, v in ipairs(hauling_route.vehicle_stops) do
+      vehicle_index = v
+    end
+
+    if vehicle_index == self.first_selected_stop.stop_index then
+      hauling_route.vehicle_stops[0] = second_selected_stop_index
+    elseif vehicle_index == second_selected_stop_index then
+      hauling_route.vehicle_stops[0] = self.first_selected_stop.stop_index
+    end
+  else
+    swap_elements(
+      routes[self.first_selected_stop.route_index].stops,
+      self.first_selected_stop.stop_index,
+      second_selected_stop_route.stops,
+      second_selected_stop_index
+    )
+  end
+
+  swap_elements(view_stops, self.first_selected_stop.list_position, nil, index - 1)
+end
+
+function ReorderStopsWindow:resetStopProperties(item)
+  local hauling = df.global.plotinfo.hauling
+  local routes = hauling.routes
+  local item_route = routes[item.route_index]
+  local same_route = self.first_selected_stop.route_index == item.route_index
+
+  for i, stop in ipairs(item_route.stops) do
+    stop.id = i + 1
+    reset_guide_paths(stop.conditions)
+  end
+
+  if not same_route and self.first_selected_stop then
+    for i, stop in ipairs(routes[self.first_selected_stop.route_index].stops) do
+      stop.id = i + 1
+      reset_guide_paths(stop.conditions)
+    end
+  end
+end
+
+function ReorderStopsWindow:init()
+  self.first_selected_stop = nil
+  self:addviews{
+    widgets.Label{
+      frame={t=0,l=0},
+      view_id='hint',
+      text=SELECT_STOP_HINT,
+    },
+    widgets.List{
+      view_id='routes',
+      frame={t=2,l=1},
+      choices={},
+      on_select=function(_, item)
+        if not item then return end
+        if item.type == 'stop' then
+          local item_pos = df.global.plotinfo.hauling.routes[item.route_index].stops[item.stop_index].pos
+          dfhack.gui.revealInDwarfmodeMap(item_pos, true, true)
+        end
+      end,
+      on_submit=function(index, item)
+        self:handleStopSelection(index, item)
+      end,
+    },
+  }
+
+  self:updateList()
+end
+
+function ReorderStopsWindow:updateList()
+  local routes = df.global.plotinfo.hauling.routes
+  local choices = {}
+  local list_position = 0
+
+  if self.first_selected_stop then
+    self.subviews.hint:setText(SELECT_ANOTHER_STOP_HINT)
+  else
+    self.subviews.hint:setText(SELECT_STOP_HINT)
+  end
+
+  for i, route in ipairs(routes) do
+    local stops = route.stops
+    local route_name = route.name
+
+    if route_name == '' then
+      route_name = 'Route ' .. route.id
+    end
+
+    table.insert(choices, {text=route_name, type='route', route_index=i, list_position=list_position})
+    list_position = list_position + 1
+
+    for j, stop in ipairs(stops) do
+      local stop_name = stop.name
+
+      if stop_name == '' then
+        stop_name = 'Stop ' .. stop.id
+      end
+
+      if self.first_selected_stop and self.first_selected_stop.list_position == list_position then
+        stop_name = '=> ' .. stop_name
+      end
+
+      stop_name = '  ' .. stop_name
+
+      table.insert(choices, {text=stop_name, type='stop', stop_index=j, route_index=i, list_position=list_position})
+      list_position = list_position + 1
+    end
+  end
+
+  self.subviews.routes:setChoices(choices)
+end
+
+function ReorderStopsWindow:onInput(keys)
+  if keys.LEAVESCREEN or keys._MOUSE_R then
+    if self.first_selected_stop then
+      self.first_selected_stop = nil
+      self:updateList()
+      return true
+    end
+  end
+
+  return ReorderStopsWindow.super.onInput(self, keys)
+end
+
+ReorderStopsModal = defclass(ReorderStopsModal, gui.ZScreenModal)
+
+ReorderStopsModal.ATTRS = {
+  focus_path = 'ReorderStops',
+}
+
+function ReorderStopsModal:init()
+  self:addviews{ReorderStopsWindow{}}
+end
+
+function ReorderStopsModal:onDismiss()
+  df.global.game.main_interface.recenter_indicator_m.x = -30000
+  df.global.game.main_interface.recenter_indicator_m.y = -30000
+  df.global.game.main_interface.recenter_indicator_m.z = -30000
+end
+
+ReorderStopsOverlay = defclass(ReorderStopsOverlay, overlay.OverlayWidget)
+ReorderStopsOverlay.ATTRS{
+  default_pos={x=6, y=6},
+  default_enabled=true,
+  viewscreens='dwarfmode/Hauling',
+  frame={w=30, h=1},
+  frame_background=gui.CLEAR_PEN,
+}
+
+function ReorderStopsOverlay:init()
+  self:addviews{
+    widgets.TextButton{
+      frame={t=0, l=0},
+      label='DFHack reorder stops',
+      key='CUSTOM_CTRL_E',
+      on_activate=function() ReorderStopsModal{}:show() end,
+    },
+  }
+end
+
 OVERLAY_WIDGETS = {
   trackstop=TrackStopOverlay,
   rollers=RollerOverlay,
+  reorderstops=ReorderStopsOverlay,
 }