Map Route to Multiple Locations (2025 Guide)
How to Create a Map Route to Multiple Locations: The Technical Guide
Need to plan delivery routes, road trips, or multi-stop journeys? While Google Maps handles simple A-to-B routes, creating optimized routes with multiple stops requires programming. Here’s the developer method for building custom multi-location route planners.
Method 1: Custom Google Maps Routes with Optimization
This solution creates an interactive route planner with multiple stops, distance calculation, and basic optimization.
Step 1: Set Up Google Cloud with Additional APIs
- 1. Create a project at [Google Cloud Console](https://console.cloud.google.com/)
- 2. Enable billing (free credits cover moderate usage)
- 3. Enable these essential APIs
- – Maps JavaScript API (core maps)
- – Directions API (route calculations)
- – Distance Matrix API (multi-point distance calculations)
- – Geocoding API (address conversion)

Step 2: Generate and Secure API Keys
- 1. Go to Credentials → Create Credentials → API Key
- 2. Critical Security Step: Restrict your key:
- – Application: HTTP referrers
- – Domains: `*.yourwebsite.com/*`
- – APIs: Restrict to only the four APIs above

Step 3: Build the Advanced Route Planner
Create `multi-location-route.html` with this comprehensive code:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Advanced Multi-Location Route Planner</title>
<style>
:root {
--primary-color: #1a73e8;
--secondary-color: #0d47a1;
--success-color: #34a853;
--warning-color: #fbbc04;
--danger-color: #ea4335;
--light-bg: #f8f9fa;
--dark-text: #202124;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Google Sans', -apple-system, BlinkMacSystemFont, sans-serif;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
min-height: 100vh;
padding: 20px;
color: var(--dark-text);
}
.container {
max-width: 1600px;
margin: 0 auto;
}
.app-header {
background: white;
border-radius: 20px 20px 0 0;
padding: 30px 40px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
margin-bottom: 2px;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 20px;
}
.header-title h1 {
font-size: 2.8rem;
font-weight: 700;
background: linear-gradient(90deg, var(--primary-color), var(--secondary-color));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 10px;
}
.header-title p {
color: #5f6368;
font-size: 1.1rem;
max-width: 700px;
}
.header-actions {
display: flex;
gap: 15px;
}
.app-main {
display: grid;
grid-template-columns: 400px 1fr;
gap: 2px;
background: white;
border-radius: 0 0 20px 20px;
overflow: hidden;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
}
.control-panel {
background: var(--light-bg);
padding: 30px;
border-right: 1px solid #e0e0e0;
}
.panel-section {
margin-bottom: 40px;
}
.section-title {
font-size: 1.3rem;
font-weight: 600;
color: var(--dark-text);
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid var(--primary-color);
display: flex;
align-items: center;
gap: 10px;
}
.section-title i {
color: var(--primary-color);
}
.waypoint-list {
max-height: 400px;
overflow-y: auto;
margin-bottom: 20px;
padding-right: 10px;
}
.waypoint-item {
background: white;
padding: 15px;
border-radius: 12px;
margin-bottom: 12px;
border: 2px solid #e0e0e0;
transition: all 0.3s;
cursor: move;
}
.waypoint-item:hover {
border-color: var(--primary-color);
transform: translateX(5px);
box-shadow: 0 5px 15px rgba(26, 115, 232, 0.1);
}
.waypoint-item.dragging {
opacity: 0.5;
background: #e8f0fe;
}
.waypoint-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.waypoint-number {
background: var(--primary-color);
color: white;
width: 28px;
height: 28px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 0.9rem;
}
.waypoint-actions {
display: flex;
gap: 8px;
}
.action-btn {
width: 32px;
height: 32px;
border-radius: 50%;
border: none;
background: #f1f3f4;
color: #5f6368;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.action-btn:hover {
background: #e8f0fe;
color: var(--primary-color);
transform: scale(1.1);
}
.waypoint-address {
font-size: 0.95rem;
color: #5f6368;
line-height: 1.4;
}
.input-group {
margin-bottom: 20px;
}
.input-group label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: var(--dark-text);
}
.address-input {
display: flex;
gap: 10px;
}
.address-input input {
flex: 1;
padding: 14px 16px;
border: 2px solid #dadce0;
border-radius: 10px;
font-size: 1rem;
transition: border-color 0.3s;
}
.address-input input:focus {
outline: none;
border-color: var(--primary-color);
}
.btn {
padding: 14px 28px;
border: none;
border-radius: 10px;
font-weight: 600;
font-size: 1rem;
cursor: pointer;
transition: all 0.3s;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.btn-primary {
background: var(--primary-color);
color: white;
}
.btn-primary:hover {
background: var(--secondary-color);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(26, 115, 232, 0.3);
}
.btn-success {
background: var(--success-color);
color: white;
}
.btn-warning {
background: var(--warning-color);
color: white;
}
.btn-danger {
background: var(--danger-color);
color: white;
}
.btn-full {
width: 100%;
margin-top: 10px;
}
.btn-group {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.optimization-controls {
background: white;
padding: 20px;
border-radius: 12px;
margin-top: 20px;
border: 2px solid #e0e0e0;
}
.optimization-option {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 15px;
padding: 10px;
border-radius: 8px;
transition: background 0.3s;
}
.optimization-option:hover {
background: #f8f9fa;
}
.optimization-option input[type="radio"] {
accent-color: var(--primary-color);
}
.optimization-option label {
flex: 1;
cursor: pointer;
}
.map-container {
position: relative;
padding: 20px;
}
#map {
height: 700px;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}
.route-info {
position: absolute;
top: 40px;
right: 40px;
background: white;
padding: 25px;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
z-index: 1000;
width: 320px;
max-height: 80vh;
overflow-y: auto;
}
.route-summary {
margin-bottom: 25px;
}
.summary-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
}
.summary-item:last-child {
border-bottom: none;
}
.summary-label {
color: #5f6368;
font-weight: 500;
}
.summary-value {
font-weight: 600;
color: var(--dark-text);
font-size: 1.1rem;
}
.directions-list {
max-height: 300px;
overflow-y: auto;
padding-right: 10px;
}
.direction-step {
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
font-size: 0.9rem;
line-height: 1.5;
}
.direction-step:last-child {
border-bottom: none;
}
.direction-icon {
color: var(--primary-color);
margin-right: 8px;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #9aa0a6;
}
.empty-state i {
font-size: 4rem;
margin-bottom: 20px;
opacity: 0.3;
}
.empty-state h3 {
font-size: 1.5rem;
margin-bottom: 10px;
color: #5f6368;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.9);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 2000;
border-radius: 15px;
display: none;
}
.spinner {
width: 50px;
height: 50px;
border: 5px solid #f3f3f3;
border-top: 5px solid var(--primary-color);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.optimization-badge {
background: var(--warning-color);
color: white;
padding: 4px 10px;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 500;
margin-left: 10px;
}
@media (max-width: 1200px) {
.app-main {
grid-template-columns: 1fr;
}
.control-panel {
border-right: none;
border-bottom: 1px solid #e0e0e0;
}
.route-info {
position: relative;
top: auto;
right: auto;
width: 100%;
margin-top: 20px;
max-height: none;
}
}
@media (max-width: 768px) {
.header-content {
flex-direction: column;
text-align: center;
}
.header-actions {
justify-content: center;
flex-wrap: wrap;
}
.btn-group {
justify-content: center;
}
.map-container {
padding: 10px;
}
#map {
height: 500px;
}
}
</style>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link href="https://fonts.googleapis.com/css2?family=Google+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
</head>
<body>
<div class="container">
<div class="app-header">
<div class="header-content">
<div class="header-title">
<h1><i class="fas fa-route"></i> Advanced Multi-Location Route Planner</h1>
<p>Plan, optimize, and visualize routes with multiple stops. Perfect for deliveries, road trips, and service routes.</p>
</div>
<div class="header-actions">
<button id="exportRouteBtn" class="btn btn-primary">
<i class="fas fa-file-export"></i> Export Route
</button>
<button id="printRouteBtn" class="btn btn-secondary">
<i class="fas fa-print"></i> Print
</button>
</div>
</div>
</div>
<div class="app-main">
<div class="control-panel">
<div class="panel-section">
<h3 class="section-title"><i class="fas fa-map-pin"></i> Route Stops</h3>
<div class="waypoint-list" id="waypointList">
<!-- Waypoints will be added here -->
<div class="empty-state">
<i class="fas fa-route"></i>
<h3>No stops added yet</h3>
<p>Add your first stop below to begin planning</p>
</div>
</div>
<div class="input-group">
<label for="newStopInput"><i class="fas fa-plus-circle"></i> Add New Stop</label>
<div class="address-input">
<input type="text"
id="newStopInput"
placeholder="Enter address or search location..."
autocomplete="off">
<button id="addStopBtn" class="btn btn-primary" style="padding: 0 20px;">
<i class="fas fa-plus"></i> Add
</button>
</div>
</div>
<div class="btn-group">
<button id="calculateRouteBtn" class="btn btn-success">
<i class="fas fa-calculator"></i> Calculate Route
</button>
<button id="optimizeRouteBtn" class="btn btn-warning">
<i class="fas fa-magic"></i> Optimize Route
</button>
<button id="clearRouteBtn" class="btn btn-danger">
<i class="fas fa-trash"></i> Clear All
</button>
</div>
</div>
<div class="panel-section">
<h3 class="section-title"><i class="fas fa-cogs"></i> Route Settings</h3>
<div class="optimization-controls">
<h4 style="margin-bottom: 15px; color: var(--dark-text);">Optimization Mode</h4>
<div class="optimization-option">
<input type="radio" id="optFastest" name="optimization" value="fastest" checked>
<label for="optFastest">
<strong>Fastest Route</strong>
<div style="font-size: 0.9rem; color: #5f6368; margin-top: 5px;">
Minimizes travel time considering current traffic
</div>
</label>
</div>
<div class="optimization-option">
<input type="radio" id="optShortest" name="optimization" value="shortest">
<label for="optShortest">
<strong>Shortest Distance</strong>
<div style="font-size: 0.9rem; color: #5f6368; margin-top: 5px;">
Minimizes total distance regardless of traffic
</div>
</label>
</div>
<div class="optimization-option">
<input type="radio" id="optAvoidHighways" name="optimization" value="avoidHighways">
<label for="optAvoidHighways">
<strong>Avoid Highways</strong>
<div style="font-size: 0.9rem; color: #5f6368; margin-top: 5px;">
Prefers local roads and avoids highways
</div>
</label>
</div>
<div class="optimization-option">
<input type="radio" id="optAvoidTolls" name="optimization" value="avoidTolls">
<label for="optAvoidTolls">
<strong>Avoid Tolls</strong>
<div style="font-size: 0.9rem; color: #5f6368; margin-top: 5px;">
Routes around toll roads when possible
</div>
</label>
</div>
</div>
<div style="margin-top: 25px;">
<h4 style="margin-bottom: 15px; color: var(--dark-text);">Travel Mode</h4>
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
<button id="modeDriving" class="btn btn-primary" data-mode="DRIVING">
<i class="fas fa-car"></i> Driving
</button>
<button id="modeWalking" class="btn btn-secondary" data-mode="WALKING">
<i class="fas fa-walking"></i> Walking
</button>
<button id="modeBicycling" class="btn btn-secondary" data-mode="BICYCLING">
<i class="fas fa-bicycle"></i> Bicycling
</button>
</div>
</div>
</div>
<div class="panel-section">
<h3 class="section-title"><i class="fas fa-history"></i> Recent Routes</h3>
<div id="recentRoutes" style="color: #5f6368; font-size: 0.95rem;">
<p>No recent routes saved. Routes will appear here after calculation.</p>
</div>
</div>
</div>
<div class="map-container">
<div id="map"></div>
<div class="route-info" id="routeInfo" style="display: none;">
<h3 style="margin-bottom: 20px; color: var(--dark-text);">
<i class="fas fa-info-circle"></i> Route Summary
</h3>
<div class="route-summary">
<div class="summary-item">
<span class="summary-label">Total Distance:</span>
<span class="summary-value" id="totalDistance">0 mi</span>
</div>
<div class="summary-item">
<span class="summary-label">Total Duration:</span>
<span class="summary-value" id="totalDuration">0 min</span>
</div>
<div class="summary-item">
<span class="summary-label">Number of Stops:</span>
<span class="summary-value" id="stopCount">0</span>
</div>
<div class="summary-item">
<span class="summary-label">Fuel Estimate:</span>
<span class="summary-value" id="fuelEstimate">0 gal</span>
</div>
</div>
<h4 style="margin: 25px 0 15px 0; color: var(--dark-text);">Turn-by-Turn Directions</h4>
<div class="directions-list" id="directionsList">
<!-- Directions will be added here -->
</div>
</div>
<div class="loading-overlay" id="loadingOverlay">
<div class="spinner"></div>
<h3>Calculating optimal route...</h3>
<p style="color: #5f6368; margin-top: 10px;">This may take a moment</p>
</div>
</div>
</div>
</div>
<!-- Load Google Maps API with required libraries -->
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY_HERE&libraries=places,geometry&callback=initMap" async defer></script>
<script>
// Configuration
let map;
let directionsService;
let directionsRenderer;
let waypoints = [];
let travelMode = 'DRIVING';
let currentRoute = null;
let autocomplete;
let dragStartIndex = null;
// Initialize the application
function initMap() {
// Default center (USA)
const defaultCenter = { lat: 39.8283, lng: -98.5795 };
// Initialize map
map = new google.maps.Map(document.getElementById("map"), {
center: defaultCenter,
zoom: 4,
mapTypeControl: true,
streetViewControl: true,
fullscreenControl: true,
mapTypeControlOptions: {
style: google.maps.MapTypeControlStyle.HORIZONTAL_BAR,
position: google.maps.ControlPosition.TOP_RIGHT
},
styles: [
{
featureType: "poi.business",
stylers: [{ visibility: "off" }]
}
]
});
// Initialize Directions services
directionsService = new google.maps.DirectionsService();
directionsRenderer = new google.maps.DirectionsRenderer({
map: map,
suppressMarkers: false,
preserveViewport: false,
polylineOptions: {
strokeColor: '#1a73e8',
strokeOpacity: 0.8,
strokeWeight: 6
},
markerOptions: {
icon: {
path: google.maps.SymbolPath.CIRCLE,
fillColor: '#1a73e8',
fillOpacity: 1,
strokeWeight: 2,
strokeColor: '#ffffff',
scale: 8
}
}
});
// Initialize autocomplete for address input
autocomplete = new google.maps.places.Autocomplete(
document.getElementById('newStopInput'),
{
types: ['address'],
componentRestrictions: { country: 'us' }
}
);
// Setup event listeners
setupEventListeners();
setupDragAndDrop();
// Load sample stops for demonstration
setTimeout(loadSampleStops, 1000);
}
function setupEventListeners() {
// Add stop button
document.getElementById('addStopBtn').addEventListener('click', addStopFromInput);
// Enter key in input field
document.getElementById('newStopInput').addEventListener('keypress', (e) => {
if (e.key === 'Enter') addStopFromInput();
});
// Route calculation buttons
document.getElementById('calculateRouteBtn').addEventListener('click', calculateRoute);
document.getElementById('optimizeRouteBtn').addEventListener('click', optimizeRoute);
document.getElementById('clearRouteBtn').addEventListener('click', clearAllStops);
// Export and print
document.getElementById('exportRouteBtn').addEventListener('click', exportRoute);
document.getElementById('printRouteBtn').addEventListener('click', printRoute);
// Travel mode buttons
document.querySelectorAll('[data-mode]').forEach(btn => {
btn.addEventListener('click', function() {
travelMode = this.getAttribute('data-mode');
// Update button styles
document.querySelectorAll('[data-mode]').forEach(b => {
b.classList.remove('btn-primary');
b.classList.add('btn-secondary');
});
this.classList.remove('btn-secondary');
this.classList.add('btn-primary');
// Recalculate route if we have one
if (waypoints.length >= 2) {
calculateRoute();
}
});
});
// Set initial active mode
document.getElementById('modeDriving').classList.add('btn-primary');
}
function setupDragAndDrop() {
const waypointList = document.getElementById('waypointList');
// Drag start
waypointList.addEventListener('dragstart', (e) => {
if (e.target.classList.contains('waypoint-item')) {
dragStartIndex = Array.from(waypointList.children).indexOf(e.target);
e.target.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
}
});
// Drag over
waypointList.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
const draggingItem = document.querySelector('.dragging');
if (!draggingItem) return;
const siblings = [...waypointList.querySelectorAll('.waypoint-item:not(.dragging)')];
const nextSibling = siblings.find(sibling => {
return e.clientY <= sibling.getBoundingClientRect().top + sibling.offsetHeight / 2;
});
waypointList.insertBefore(draggingItem, nextSibling);
});
// Drag end
waypointList.addEventListener('dragend', (e) => {
const draggingItem = document.querySelector('.dragging');
if (!draggingItem) return;
draggingItem.classList.remove('dragging');
const dragEndIndex = Array.from(waypointList.children).indexOf(draggingItem);
if (dragStartIndex !== null && dragStartIndex !== dragEndIndex) {
// Reorder waypoints array
const movedWaypoint = waypoints.splice(dragStartIndex, 1)[0];
waypoints.splice(dragEndIndex, 0, movedWaypoint);
// Update waypoint numbers
updateWaypointList();
// Recalculate route if we have enough stops
if (waypoints.length >= 2) {
calculateRoute();
}
}
dragStartIndex = null;
});
}
function addStopFromInput() {
const input = document.getElementById('newStopInput');
const address = input.value.trim();
if (!address) {
alert('Please enter an address');
return;
}
// Use geocoding to get coordinates
const geocoder = new google.maps.Geocoder();
showLoading(true);
geocoder.geocode({ address: address }, (results, status) => {
showLoading(false);
if (status === 'OK') {
const location = results[0].geometry.location;
const formattedAddress = results[0].formatted_address;
addStop(location.lat(), location.lng(), formattedAddress);
input.value = '';
// If this is the second stop, automatically calculate route
if (waypoints.length === 2) {
setTimeout(calculateRoute, 500);
}
} else {
alert('Could not find location. Please try a different address.');
}
});
}
function addStop(lat, lng, address) {
const waypoint = {
id: Date.now() + Math.random(),
lat: lat,
lng: lng,
address: address,
added: new Date().toISOString()
};
waypoints.push(waypoint);
updateWaypointList();
// Center map on new stop if it's the first one
if (waypoints.length === 1) {
map.setCenter({ lat: lat, lng: lng });
map.setZoom(12);
}
}
function updateWaypointList() {
const waypointList = document.getElementById('waypointList');
if (waypoints.length === 0) {
waypointList.innerHTML = `
<div class="empty-state">
<i class="fas fa-route"></i>
<h3>No stops added yet</h3>
<p>Add your first stop below to begin planning</p>
</div>
`;
return;
}
let html = '';
waypoints.forEach((waypoint, index) => {
html += `
<div class="waypoint-item" draggable="true">
<div class="waypoint-header">
<div class="waypoint-number">${index + 1}</div>
<div class="waypoint-actions">
<button onclick="zoomToStop(${index})" class="action-btn" title="Zoom to stop">
<i class="fas fa-search-plus"></i>
</button>
<button onclick="removeStop(${index})" class="action-btn" title="Remove stop">
<i class="fas fa-times"></i>
</button>
</div>
</div>
<div class="waypoint-address">${waypoint.address}</div>
</div>
`;
});
waypointList.innerHTML = html;
}
function zoomToStop(index) {
if (waypoints[index]) {
map.setCenter({ lat: waypoints[index].lat, lng: waypoints[index].lng });
map.setZoom(14);
}
}
function removeStop(index) {
if (confirm('Remove this stop from the route?')) {
waypoints.splice(index, 1);
updateWaypointList();
// Recalculate route if we still have enough stops
if (waypoints.length >= 2) {
calculateRoute();
} else {
// Clear route display
directionsRenderer.setDirections({ routes: [] });
document.getElementById('routeInfo').style.display = 'none';
}
}
}
function calculateRoute() {
if (waypoints.length < 2) {
alert('Please add at least two stops to calculate a route.');
return;
}
showLoading(true);
// Prepare request for Directions API
const origin = waypoints[0];
const destination = waypoints[waypoints.length - 1];
const middleWaypoints = waypoints.slice(1, -1).map(wp => ({
location: new google.maps.LatLng(wp.lat, wp.lng),
stopover: true
}));
// Get optimization mode
const optimization = document.querySelector('input[name="optimization"]:checked').value;
// Build request with optimization options
const request = {
origin: new google.maps.LatLng(origin.lat, origin.lng),
destination: new google.maps.LatLng(destination.lat, destination.lng),
waypoints: middleWaypoints,
travelMode: google.maps.TravelMode[travelMode],
optimizeWaypoints: (optimization === 'fastest'),
provideRouteAlternatives: false,
avoidHighways: (optimization === 'avoidHighways'),
avoidTolls: (optimization === 'avoidTolls'),
unitSystem: google.maps.UnitSystem.IMPERIAL
};
// Make API call
directionsService.route(request, (result, status) => {
showLoading(false);
if (status === 'OK') {
directionsRenderer.setDirections(result);
currentRoute = result.routes[0];
displayRouteInfo(result.routes[0]);
saveToRecentRoutes();
} else {
alert('Could not calculate route. Error: ' + status);
}
});
}
function optimizeRoute() {
if (waypoints.length < 3) {
alert('Optimization requires at least 3 stops.');
return;
}
showLoading(true);
// For true optimization, we'd use the Distance Matrix API and TSP algorithms
// This is a simplified version that reorders stops based on proximity
// Extract middle waypoints (excluding first and last)
const middlePoints = waypoints.slice(1, -1);
const optimizedMiddle = [];
// Simple nearest neighbor algorithm
let currentPoint = waypoints[0];
let remainingPoints = [...middlePoints];
while (remainingPoints.length > 0) {
// Find closest point to current point
let closestIndex = 0;
let closestDistance = Number.MAX_VALUE;
remainingPoints.forEach((point, index) => {
const distance = calculateDistance(
currentPoint.lat, currentPoint.lng,
point.lat, point.lng
);
if (distance < closestDistance) {
closestDistance = distance;
closestIndex = index;
}
});
// Add closest point to optimized route
optimizedMiddle.push(remainingPoints[closestIndex]);
currentPoint = remainingPoints[closestIndex];
remainingPoints.splice(closestIndex, 1);
}
// Reconstruct waypoints array
const optimizedWaypoints = [
waypoints[0],
...optimizedMiddle,
waypoints[waypoints.length - 1]
];
waypoints = optimizedWaypoints;
updateWaypointList();
// Show optimization badge
const optimizeBtn = document.getElementById('optimizeRouteBtn');
optimizeBtn.innerHTML = '<i class="fas fa-check"></i> Optimized!';
optimizeBtn.classList.remove('btn-warning');
optimizeBtn.classList.add('btn-success');
setTimeout(() => {
optimizeBtn.innerHTML = '<i class="fas fa-magic"></i> Optimize Route';
optimizeBtn.classList.remove('btn-success');
optimizeBtn.classList.add('btn-warning');
}, 3000);
// Recalculate route with optimized waypoints
calculateRoute();
}
function displayRouteInfo(route) {
const legs = route.legs;
let totalDistance = 0;
let totalDuration = 0;
// Calculate totals
legs.forEach(leg => {
totalDistance += leg.distance.value; // in meters
totalDuration += leg.duration.value; // in seconds
});
// Convert distance to miles
const distanceMiles = (totalDistance / 1609.34).toFixed(1);
// Convert duration to hours/minutes
const hours = Math.floor(totalDuration / 3600);
const minutes = Math.floor((totalDuration % 3600) / 60);
// Calculate fuel estimate (assuming 25 MPG)
const fuelGallons = (distanceMiles / 25).toFixed(1);
// Update summary
document.getElementById('totalDistance').textContent = `${distanceMiles} mi`;
document.getElementById('totalDuration').textContent = hours > 0
? `${hours}h ${minutes}m`
: `${minutes} min`;
document.getElementById('stopCount').textContent = waypoints.length;
document.getElementById('fuelEstimate').textContent = `${fuelGallons} gal`;
// Build directions list
let directionsHtml = '';
let stepNumber = 1;
legs.forEach((leg, legIndex) => {
// Add start of leg
directionsHtml += `
<div class="direction-step">
<i class="fas fa-flag-checkered direction-icon"></i>
<strong>Start: ${leg.start_address}</strong>
</div>
`;
// Add steps
leg.steps.forEach(step => {
const instruction = step.instructions.replace(/<[^>]*>/g, '');
directionsHtml += `
<div class="direction-step">
<i class="fas fa-arrow-right direction-icon"></i>
<span>${stepNumber}. ${instruction}</span>
</div>
`;
stepNumber++;
});
// Add end of leg (unless it's the last leg)
if (legIndex < legs.length - 1) {
directionsHtml += `
<div class="direction-step">
<i class="fas fa-map-marker-alt direction-icon"></i>
<strong>Arrive at: ${leg.end_address}</strong>
</div>
`;
}
});
document.getElementById('directionsList').innerHTML = directionsHtml;
// Show route info panel
document.getElementById('routeInfo').style.display = 'block';
}
function clearAllStops() {
if (waypoints.length === 0) return;
if (confirm(`Clear all ${waypoints.length} stops?`)) {
waypoints = [];
updateWaypointList();
directionsRenderer.setDirections({ routes: [] });
document.getElementById('routeInfo').style.display = 'none';
}
}
function exportRoute() {
if (!currentRoute) {
alert('No route to export. Calculate a route first.');
return;
}
const exportData = {
waypoints: waypoints.map((wp, index) => ({
stopNumber: index + 1,
address: wp.address,
latitude: wp.lat,
longitude: wp.lng
})),
routeSummary: {
totalDistance: document.getElementById('totalDistance').textContent,
totalDuration: document.getElementById('totalDuration').textContent,
numberOfStops: waypoints.length,
travelMode: travelMode,
calculatedAt: new Date().toISOString()
},
directions: currentRoute.legs.flatMap(leg =>
leg.steps.map(step => ({
instruction: step.instructions.replace(/<[^>]*>/g, ''),
distance: step.distance.text,
duration: step.duration.text
}))
)
};
const dataStr = JSON.stringify(exportData, null, 2);
const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr);
const linkElement = document.createElement('a');
linkElement.setAttribute('href', dataUri);
linkElement.setAttribute('download', `route-${new Date().toISOString().slice(0,10)}.json`);
document.body.appendChild(linkElement);
linkElement.click();
document.body.removeChild(linkElement);
}
function printRoute() {
window.print();
}
function saveToRecentRoutes() {
const recentRoutes = document.getElementById('recentRoutes');
const routeInfo = {
date: new Date().toLocaleString(),
stops: waypoints.length,
distance: document.getElementById('totalDistance').textContent,
duration: document.getElementById('totalDuration').textContent
};
const routeHtml = `
<div style="background: white; padding: 12px; border-radius: 8px; margin-bottom: 10px; border-left: 4px solid var(--primary-color);">
<div style="font-weight: 600; margin-bottom: 5px;">Route from ${waypoints[0].address.split(',')[0]} to ${waypoints[waypoints.length-1].address.split(',')[0]}</div>
<div style="display: flex; justify-content: space-between; font-size: 0.85rem; color: #5f6368;">
<span>${routeInfo.stops} stops</span>
<span>${routeInfo.distance}</span>
<span>${routeInfo.duration}</span>
</div>
<div style="font-size: 0.8rem; color: #9aa0a6; margin-top: 5px;">${routeInfo.date}</div>
</div>
`;
// Add to beginning of list
recentRoutes.innerHTML = routeHtml + recentRoutes.innerHTML;
}
function showLoading(show) {
document.getElementById('loadingOverlay').style.display = show ? 'flex' : 'none';
}
function calculateDistance(lat1, lon1, lat2, lon2) {
const R = 6371; // Earth's radius in km
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLon = (lon2 - lon1) * Math.PI / 180;
const a =
Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLon/2) * Math.sin(dLon/2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
return R * c; // Distance in km
}
function loadSampleStops() {
if (waypoints.length > 0) return;
const sampleStops = [
{ lat: 40.7128, lng: -74.0060, address: "New York, NY, USA" },
{ lat: 39.9526, lng: -75.1652, address: "Philadelphia, PA, USA" },
{ lat: 38.9072, lng: -77.0369, address: "Washington, DC, USA" },
{ lat: 39.2904, lng: -76.6122, address: "Baltimore, MD, USA" }
];
sampleStops.forEach((stop, index) => {
setTimeout(() => {
addStop(stop.lat, stop.lng, stop.address);
// Auto-calculate after adding all stops
if (index === sampleStops.length - 1) {
setTimeout(calculateRoute, 1000);
}
}, index * 300);
});
}
// Make functions available globally
window.zoomToStop = zoomToStop;
window.removeStop = removeStop;
</script>
</body>
</html>
Code language: HTML, XML (xml)
Step 4: Deploy and Use Your Route Planner
- 1. Replace API Key: Change `YOUR_API_KEY_HERE` to your actual Google Maps API key
- 2. Test Locally: Open in browser and test with sample stops
- 3. Deploy to Server: Upload to your web hosting environment
- 4. Share with Team: Provide access to colleagues or embed in internal tools
The Hidden Costs and Complexities
While this custom solution is powerful, it comes with significant challenges:
- 1. Multiple API Costs: Directions API and Distance Matrix API incur separate charges per request
- 2. Complex Optimization: True route optimization requires advanced algorithms (Traveling Salesman Problem)
- 3. Rate Limiting: Google imposes strict limits on API calls (50 requests per second)
- 4. No Real Traffic Integration: Advanced traffic-aware routing requires additional APIs
- 5. No Historical Data Can’t analyze past routes or performance
- 6. No Multi-Day Planning: Can’t schedule routes across multiple days
- 7. No Driver Management: No way to assign routes to specific drivers or vehicles
- 8. Mobile Limitations: Complex interface doesn’t work well for field staff
The Professional Alternative: MapsFun.com
Why spend weeks building and maintaining a route planner when you can have an enterprise solution instantly?
MapsFun.com provides complete route planning and optimization without any coding:
- ✅ True Route Optimization – Advanced algorithms for optimal stop sequencing
- ✅ Multi-Day Planning – Schedule routes across days with driver assignments
- ✅ Real Traffic Integration – Live traffic-aware routing
- ✅ Unlimited Waypoints – Plan routes with hundreds of stops
- ✅ Driver Mobile App – Turn-by-turn navigation for field staff
- ✅ Proof of Delivery – Digital signatures and photo capture
- ✅ Analytics Dashboard- Track performance and efficiency metrics
- ✅ Team Collaboration- Multiple planners can work simultaneously
With MapsFun.com, here’s the complete workflow:
- 1. Import addresses from CSV or spreadsheet
- 2. Set constraints (time windows, driver schedules, vehicle capacities)
- 3. Click “Optimize” – AI calculates the most efficient routes
- 4. Dispatch to drivers via mobile app
- 5. Track in real-time and analyze performance
The custom Google Maps solution works for simple A-to-B-to-C routing, but for delivery companies, field service businesses, sales teams, or anyone needing true multi-location route optimization, it’s insufficient and requires months of additional development.
Stop building route planners and start using professional ones. Plan, optimize, and execute multi-location routes in minutes at MapsFun.com – the complete route optimization platform.