Road Map Shortest Route Simulation: A Simple Python Desktop App for Smarter Navigation
Road Map Shortest Route Simulation: A Simple Python Desktop App for Smarter Navigation
Finding the shortest and most efficient route between two locations is an essential problem in many fields—transportation, logistics, city planning, delivery services, and even gaming. To help beginners and professionals experiment with this concept, the Road Map Shortest Route Simulation App provides an easy and interactive way to visualize how shortest-path algorithms work in real life.
This desktop application, built using Python, allows users to create cities, connect them with roads, and instantly calculate the shortest path using Dijkstra’s Algorithm. The interface is easy to use, and you can run the app directly in Visual Studio Code.
🚀 What This App Does
The Road Map Shortest Route Simulation app lets you:
-
Add cities (nodes) by clicking on the canvas
-
Connect cities with roads and distances
-
Select a start and end point
-
Compute the shortest route
-
Visualize the result with a highlighted path
-
Reset the entire map anytime
This makes it perfect for:
-
Students learning graph algorithms
-
Beginners practicing Python desktop GUIs
-
Developers simulating route networks
-
Bloggers or teachers explaining shortest-path logic
🧭 How the App Works
The application is based on graph theory. Each city you add becomes a node, and every road becomes an edge with a specific distance. When you choose a start and destination city, the app uses Dijkstra’s Algorithm to compute the minimum total distance between them.
The final route is displayed visually, making it extremely easy to understand the path selection process.
🔧 Key Features of the App
✔ 1. Add Cities with One Click
Simply click on the canvas to place a new city. Each city is automatically named and positioned exactly where you click.
✔ 2. Draw Roads Between Cities
Select the “Connect Cities” option, click two cities, enter the distance, and a road will appear between them. The distance is displayed on the line for easy understanding.
✔ 3. Choose Starting and Ending Points
Click the “Select Start” and “Select End” buttons, then choose your cities on the map.
✔ 4. Shortest Path Calculation
When you click “Find Shortest Path,” the app immediately calculates the minimum route and shows it with a bold highlight.
✔ 5. Reset Anytime
Start fresh whenever you want with the Reset button. Build unlimited maps and test various scenarios.
🎓 Why This App Is Useful
Shortest path algorithms power everything from Google Maps to delivery routing apps. By experimenting with this lightweight desktop tool, users can:
-
Understand graph structures visually
-
Practice algorithm implementation
-
See real-time shortest route results
-
Learn how roads and distances affect navigation
It is a great educational resource, especially for computer science students preparing for interviews or competitive exams.
💡 Who Should Use This App?
This tool is ideal for:
-
Engineering students
-
Python learners
-
Teachers explaining algorithms
-
Developers testing pathfinding logic
-
Anyone curious about route optimization
Since it runs as a simple Python file, it works on any system with Python installed.
🏁 Conclusion
The Road Map Shortest Route Simulation app is both simple and powerful. It combines Python programming, GUI design, and graph algorithms in a highly interactive way. Whether you're learning, teaching, or experimenting, this app will help you understand how the shortest route between two points is calculated—and how modern navigation systems think behind the scenes.
If you’re looking to explore algorithms visually or build your own navigation tools, this app is the perfect place to start.
"""
Road Map Shortest Route Simulation
Single-file Python desktop app (Tkinter) you can run in Visual Studio Code.
Features:
- Click to add nodes (toggle Add Node mode)
- Connect nodes to create weighted edges (Add Edge mode; click two nodes)
- Set Start and Goal nodes (Set Start / Set Goal mode)
- Run Dijkstra's algorithm to compute shortest path
- Step through the algorithm visually (Step button)
- Clear / Reset / Random sample graph
How to run:
1. Save this file as `road_map_shortest_route_simulation.py` in VS Code.
2. Run: `python road_map_shortest_route_simulation.py` (requires Python 3.x)
No external libraries required.
"""
import tkinter as tk
from tkinter import simpledialog, messagebox
import heapq
import math
import random
NODE_RADIUS = 12
class Node:
def __init__(self, nid, x, y):
self.id = nid
self.x = x
self.y = y
class Edge:
def __init__(self, a, b, w=1.0):
self.a = a
self.b = b
self.w = w
class RoadMapApp:
def __init__(self, root):
self.root = root
root.title('Road Map Shortest Route Simulation')
self.canvas = tk.Canvas(root, width=900, height=600, bg='white')
self.canvas.grid(row=0, column=0, rowspan=20)
# Controls
self.mode = tk.StringVar(value='add_node')
modes = [('Add Node', 'add_node'), ('Add Edge', 'add_edge'),
('Set Start', 'set_start'), ('Set Goal', 'set_goal'), ('Move', 'move')]
r = 0
for (text, val) in modes:
b = tk.Radiobutton(root, text=text, variable=self.mode, value=val)
b.grid(row=r, column=1, sticky='w', padx=8)
r += 1
tk.Button(root, text='Set Weight for Edge', command=self.set_weight_for_edge).grid(row=r, column=1, sticky='we', padx=8)
r += 1
tk.Button(root, text='Run Dijkstra', command=self.run_dijkstra).grid(row=r, column=1, sticky='we', padx=8)
r += 1
tk.Button(root, text='Step', command=self.step_dijkstra).grid(row=r, column=1, sticky='we', padx=8)
r += 1
tk.Button(root, text='Reset Algorithm', command=self.reset_algorithm).grid(row=r, column=1, sticky='we', padx=8)
r += 1
tk.Button(root, text='Clear All', command=self.clear_all).grid(row=r, column=1, sticky='we', padx=8)
r += 1
tk.Button(root, text='Random Sample Graph', command=self.random_graph).grid(row=r, column=1, sticky='we', padx=8)
r += 1
tk.Button(root, text='Help / Instructions', command=self.show_help).grid(row=r, column=1, sticky='we', padx=8)
# Data
self.nodes = {} # id -> Node
self.edges = [] # list of Edge
self.node_counter = 0
# UI handles for items
self.node_items = {} # node id -> (oval_id, text_id)
self.edge_items = [] # list of (line_id, text_id, edge)
# For edge creation
self.edge_first = None
# For moving nodes
self.moving_node = None
# Start / Goal
self.start = None
self.goal = None
# Dijkstra state
self.dijkstra_gen = None
self.dijkstra_result = None # (distances, prev)
# Bind events
self.canvas.bind('<Button-1>', self.on_canvas_click)
self.canvas.bind('<B1-Motion>', self.on_canvas_drag)
self.canvas.bind('<ButtonRelease-1>', self.on_canvas_release)
self.draw_instructions()
# ---------- UI helper functions ----------
def draw_instructions(self):
self.canvas.delete('header')
text = 'Modes: Add Node | Add Edge | Set Start | Set Goal | Move • Click canvas to act according to mode.'
self.canvas.create_text(450, 12, text=text, tag='header', font=('Arial', 10), fill='gray')
def show_help(self):
help_text = (
'Instructions:\n'
'- Add Node: click anywhere to add a node.\n'
'- Add Edge: click two nodes to connect them; you will be prompted for weight.\n'
"- Set Start / Set Goal: choose mode and click a node.\n"
'- Move: drag a node to reposition it.\n'
'- Run Dijkstra: compute shortest path from Start to Goal.\n'
'- Step: step through Dijkstra visualization.\n'
'- Reset Algorithm: clears Dijkstra highlights but keeps graph.\n'
'- Random Sample Graph: populates a sample graph for testing.\n'
)
messagebox.showinfo('Help / Instructions', help_text)
def clear_all(self):
self.nodes.clear()
self.edges.clear()
self.node_items.clear()
for item in self.edge_items:
self.canvas.delete(item[0])
self.canvas.delete(item[1])
self.edge_items.clear()
self.canvas.delete('node')
self.canvas.delete('edge')
self.node_counter = 0
self.edge_first = None
self.start = None
self.goal = None
self.reset_algorithm()
def random_graph(self):
self.clear_all()
n = 8
margin = 60
for i in range(n):
x = random.randint(margin, 900 - margin)
y = random.randint(50 + margin, 600 - margin)
self.add_node_at(x, y)
# connect randomly
ids = list(self.nodes.keys())
for i in range(n):
a = ids[i]
for j in range(i+1, n):
if random.random() < 0.35:
b = ids[j]
w = round(math.dist((self.nodes[a].x, self.nodes[a].y), (self.nodes[b].x, self.nodes[b].y)) / 50, 2)
self.add_edge(a, b, w)
# set random start/goal
if ids:
self.start = ids[0]
self.goal = ids[-1]
self.update_node_colors()
# ---------- Node / Edge manipulation ----------
def add_node_at(self, x, y):
nid = self.node_counter
self.node_counter += 1
node = Node(nid, x, y)
self.nodes[nid] = node
oval = self.canvas.create_oval(x - NODE_RADIUS, y - NODE_RADIUS, x + NODE_RADIUS, y + NODE_RADIUS, fill='lightgray', tags=('node',))
text = self.canvas.create_text(x, y, text=str(nid), tags=('node',))
self.node_items[nid] = (oval, text)
self.canvas.tag_bind(oval, '<Button-1>', lambda e, nid=nid: self.on_node_click(nid))
self.canvas.tag_bind(text, '<Button-1>', lambda e, nid=nid: self.on_node_click(nid))
self.update_node_colors()
return nid
def find_node_at(self, x, y):
for nid, node in self.nodes.items():
if (x - node.x) ** 2 + (y - node.y) ** 2 <= NODE_RADIUS ** 2:
return nid
return None
def on_node_click(self, nid):
mode = self.mode.get()
if mode == 'add_edge':
if self.edge_first is None:
self.edge_first = nid
self.highlight_node(nid, outline='blue')
else:
if self.edge_first != nid:
w = simpledialog.askfloat('Edge weight', 'Enter weight (positive number):', minvalue=0.0)
if w is None:
# cancel
self.unhighlight_node(self.edge_first)
self.edge_first = None
return
self.add_edge(self.edge_first, nid, w)
self.unhighlight_node(self.edge_first)
self.edge_first = None
elif mode == 'set_start':
self.start = nid
self.update_node_colors()
elif mode == 'set_goal':
self.goal = nid
self.update_node_colors()
elif mode == 'move':
self.moving_node = nid
elif mode == 'add_node':
# add node near clicked location
node = self.nodes[nid]
self.add_node_at(node.x + 20, node.y + 20)
def on_canvas_click(self, event):
mode = self.mode.get()
x, y = event.x, event.y
if mode == 'add_node':
self.add_node_at(x, y)
elif mode == 'move':
nid = self.find_node_at(x, y)
if nid is not None:
self.moving_node = nid
else:
# maybe clicked empty space; clear edge_first selection
if self.edge_first is not None:
self.unhighlight_node(self.edge_first)
self.edge_first = None
def on_canvas_drag(self, event):
if self.moving_node is not None:
self.move_node_to(self.moving_node, event.x, event.y)
def on_canvas_release(self, event):
self.moving_node = None
def move_node_to(self, nid, x, y):
node = self.nodes[nid]
node.x = x
node.y = y
oval, text = self.node_items[nid]
self.canvas.coords(oval, x - NODE_RADIUS, y - NODE_RADIUS, x + NODE_RADIUS, y + NODE_RADIUS)
self.canvas.coords(text, x, y)
self.redraw_edges()
def add_edge(self, a, b, w=1.0):
# avoid duplicate
if a == b:
return
for e in self.edges:
if (e.a == a and e.b == b) or (e.a == b and e.b == a):
# update weight
e.w = w
self.redraw_edges()
return
edge = Edge(a, b, w)
self.edges.append(edge)
self.redraw_edges()
def set_weight_for_edge(self):
if not self.edges:
messagebox.showinfo('Info', 'No edges to set weight for.')
return
choices = ['{} - {} (w={})'.format(e.a, e.b, e.w) for e in self.edges]
choice = simpledialog.askinteger('Choose edge index', 'Enter edge index (0 - {}):'.format(len(self.edges)-1), minvalue=0, maxvalue=len(self.edges)-1)
if choice is None:
return
e = self.edges[choice]
w = simpledialog.askfloat('Edge weight', 'Enter new weight:', minvalue=0.0)
if w is None:
return
e.w = w
self.redraw_edges()
# ---------- Drawing edges / nodes ----------
def redraw_edges(self):
# clear existing edge items
for line_id, text_id, _ in self.edge_items:
self.canvas.delete(line_id)
self.canvas.delete(text_id)
self.edge_items.clear()
for idx, e in enumerate(self.edges):
a = self.nodes[e.a]
b = self.nodes[e.b]
line = self.canvas.create_line(a.x, a.y, b.x, b.y, width=2, tags=('edge',))
# weight label midpoint
mx = (a.x + b.x) / 2
my = (a.y + b.y) / 2
txt = self.canvas.create_text(mx, my - 10, text=str(e.w), tags=('edge',))
self.edge_items.append((line, txt, e))
self.update_node_colors()
def update_node_colors(self):
for nid, (oval, text) in self.node_items.items():
fill = 'lightgray'
outline = 'black'
if nid == self.start:
fill = 'orange'
if nid == self.goal:
fill = 'lightgreen'
self.canvas.itemconfig(oval, fill=fill, outline=outline)
def highlight_node(self, nid, outline='blue'):
if nid in self.node_items:
oval, _ = self.node_items[nid]
self.canvas.itemconfig(oval, outline=outline, width=3)
def unhighlight_node(self, nid):
if nid in self.node_items:
oval, _ = self.node_items[nid]
self.canvas.itemconfig(oval, outline='black', width=1)
# ---------- Dijkstra algorithm & visualization ----------
def reset_algorithm(self):
# Clear visualization highlights
for line_id, text_id, edge in self.edge_items:
self.canvas.itemconfig(line_id, fill='black', width=2)
for nid, (oval, text) in self.node_items.items():
self.canvas.itemconfig(oval, fill=('orange' if nid == self.start else ('lightgreen' if nid == self.goal else 'lightgray')))
self.dijkstra_gen = None
self.dijkstra_result = None
def run_dijkstra(self):
if self.start is None or self.goal is None:
messagebox.showwarning('Missing nodes', 'Please set both Start and Goal nodes before running.')
return
self.dijkstra_gen = None
dist, prev = self.dijkstra(self.start)
self.dijkstra_result = (dist, prev)
if dist.get(self.goal, math.inf) == math.inf:
messagebox.showinfo('Result', 'No path found from {} to {}'.format(self.start, self.goal))
return
path = self.reconstruct_path(prev, self.goal)
self.animate_final_path(path)
def step_dijkstra(self):
# initialize generator if needed
if self.dijkstra_gen is None:
if self.start is None or self.goal is None:
messagebox.showwarning('Missing nodes', 'Please set both Start and Goal nodes before stepping.')
return
self.dijkstra_gen = self.dijkstra_step(self.start)
try:
info = next(self.dijkstra_gen)
self.visualize_step(info)
except StopIteration as e:
# finished; maybe show path
if self.dijkstra_result is None:
# if result wasn't set, compute final result quickly
dist, prev = self.dijkstra(self.start)
self.dijkstra_result = (dist, prev)
dist, prev = self.dijkstra_result
if dist.get(self.goal, math.inf) == math.inf:
messagebox.showinfo('Result', 'No path found from {} to {}'.format(self.start, self.goal))
else:
path = self.reconstruct_path(prev, self.goal)
self.animate_final_path(path)
def visualize_step(self, info):
# info contains: visited_set, frontier_heap (as list), current_node, distances, prev
visited, frontier, current, distances, prev = info
# color visited nodes
for nid in self.nodes.keys():
oval, _ = self.node_items[nid]
if nid in visited:
self.canvas.itemconfig(oval, fill='skyblue')
else:
self.canvas.itemconfig(oval, fill=('orange' if nid == self.start else ('lightgreen' if nid == self.goal else 'lightgray')))
# highlight frontier nodes
for nid in self.nodes.keys():
if nid in [item[1] for item in frontier]:
oval, _ = self.node_items[nid]
self.canvas.itemconfig(oval, fill='yellow')
# highlight current
if current is not None:
if current in self.node_items:
oval, _ = self.node_items[current]
self.canvas.itemconfig(oval, fill='red')
# update distances on edges / nodes
for nid in self.nodes.keys():
# update node text to include distance if known
oval, text = self.node_items[nid]
d = distances.get(nid, math.inf)
display = str(nid) + (('\n{:.1f}'.format(d)) if d < math.inf else '')
self.canvas.itemconfig(text, text=display)
# color edges that are relaxed (part of prev tree)
for line_id, text_id, edge in self.edge_items:
self.canvas.itemconfig(line_id, fill='black', width=2)
for nid, p in prev.items():
if p is not None:
# find edge connecting nid and p
for line_id, text_id, edge in self.edge_items:
if (edge.a == nid and edge.b == p) or (edge.b == nid and edge.a == p):
self.canvas.itemconfig(line_id, fill='purple', width=3)
break
def animate_final_path(self, path):
# reset nodes then highlight path
for nid, (oval, text) in self.node_items.items():
self.canvas.itemconfig(oval, fill=('orange' if nid == self.start else ('lightgreen' if nid == self.goal else 'lightgray')))
self.canvas.itemconfig(text, text=str(nid))
# color edges along path
for line_id, text_id, edge in self.edge_items:
self.canvas.itemconfig(line_id, fill='black', width=2)
for i in range(len(path)-1):
a = path[i]
b = path[i+1]
for line_id, text_id, edge in self.edge_items:
if (edge.a == a and edge.b == b) or (edge.a == b and edge.b == a):
self.canvas.itemconfig(line_id, fill='green', width=4)
break
# highlight path nodes
for nid in path:
if nid in self.node_items:
oval, _ = self.node_items[nid]
self.canvas.itemconfig(oval, fill='green')
def reconstruct_path(self, prev, goal):
path = []
cur = goal
while cur is not None:
path.append(cur)
cur = prev.get(cur, None)
path.reverse()
return path
def dijkstra(self, start):
# Standard Dijkstra returning distances and predecessor map
dist = {nid: math.inf for nid in self.nodes}
prev = {nid: None for nid in self.nodes}
dist[start] = 0
heap = [(0, start)]
adj = self.build_adj()
visited = set()
while heap:
d, u = heapq.heappop(heap)
if u in visited:
continue
visited.add(u)
for v, w in adj.get(u, []):
nd = d + w
if nd < dist[v]:
dist[v] = nd
prev[v] = u
heapq.heappush(heap, (nd, v))
self.dijkstra_result = (dist, prev)
return dist, prev
def dijkstra_step(self, start):
# Generator that yields intermediate states for visualization
dist = {nid: math.inf for nid in self.nodes}
prev = {nid: None for nid in self.nodes}
dist[start] = 0
heap = [(0, start)]
adj = self.build_adj()
visited = set()
while heap:
d, u = heapq.heappop(heap)
if u in visited:
yield (visited.copy(), list(heap), None, dict(dist), dict(prev))
continue
# yield state before processing u
yield (visited.copy(), list(heap), u, dict(dist), dict(prev))
visited.add(u)
for v, w in adj.get(u, []):
nd = d + w
if nd < dist[v]:
dist[v] = nd
prev[v] = u
heapq.heappush(heap, (nd, v))
# yield after relaxation
yield (visited.copy(), list(heap), u, dict(dist), dict(prev))
# final yield
self.dijkstra_result = (dist, prev)
yield (visited.copy(), list(heap), None, dict(dist), dict(prev))
def build_adj(self):
adj = {nid: [] for nid in self.nodes}
for e in self.edges:
if e.a in adj and e.b in adj:
adj[e.a].append((e.b, e.w))
adj[e.b].append((e.a, e.w))
return adj
if __name__ == '__main__':
root = tk.Tk()
app = RoadMapApp(root)
root.mainloop()
Comments
Post a Comment