Better Route Notification
This guide will teach you how to get notified when a better route is detected during navigation.
How it works
The example app demonstrates the following key features:
- Rendering an interactive map.
- Calculating routes with enhanced detection for better alternatives.
- Simulating navigation along a predefined route.
- Providing detailed insights on newly identified routes.
The example functionality is highly dependent on current traffic conditions. If the time difference between the selected route and the others is no greater than 5 minutes, the notification will not appear. See the Better Route Detection documentation.
UI and Map Integration
The following code demonstrates how to initialize the map and create UI elements for building routes, starting simulation, and displaying navigation information. When a better route is identified, a notification modal will appear with details about the time savings.
import {
GemKit,
GemMap,
Coordinates,
PositionService,
Landmark,
RoutePreferences,
RoutingService,
NavigationService,
NavigationInstruction,
GemError,
Route
} from '@magiclane/maps-sdk';
import { GEMKIT_TOKEN } from './token';
let map: GemMap | null = null;
let currentInstruction: NavigationInstruction | null = null;
let areRoutesBuilt = false;
let isSimulationActive = false;
// We use the progress listener to cancel the route calculation.
let routingHandler: any = null;
// We use the progress listener to cancel the navigation.
let navigationHandler: any = null;
// UI Elements
let buildRouteBtn: HTMLButtonElement;
let startSimulationBtn: HTMLButtonElement;
let stopSimulationBtn: HTMLButtonElement;
let recenterBtn: HTMLButtonElement;
let navigationPanel: HTMLDivElement;
let instructionPanel: HTMLDivElement;
let bottomPanel: HTMLDivElement;
function updateUI() {
// Logic to toggle main buttons in the same spot
buildRouteBtn.style.display = 'none';
startSimulationBtn.style.display = 'none';
stopSimulationBtn.style.display = 'none';
if (!areRoutesBuilt) {
buildRouteBtn.style.display = 'block';
} else if (areRoutesBuilt && !isSimulationActive) {
startSimulationBtn.style.display = 'block';
} else if (isSimulationActive) {
stopSimulationBtn.style.display = 'block';
}
navigationPanel.style.display = isSimulationActive ? 'block' : 'none';
bottomPanel.style.display = isSimulationActive ? 'block' : 'none';
}
// Custom method for calling calculate route and displaying the results.
function onBuildRouteButtonPressed() {
// Define the departure
const departureLandmark = Landmark.withLatLng({
latitude: 48.79743778098061,
longitude: 2.4029037044571875
});
// Define the destination
const destinationLandmark = Landmark.withLatLng({
latitude: 48.904767018940184,
longitude: 2.3223936076132086
});
// Define the route preferences
const routePreferences = new RoutePreferences({});
showMessage('The route is calculating.');
// Calculate route
routingHandler = RoutingService.calculateRoute(
[departureLandmark, destinationLandmark],
routePreferences,
(err: GemError, calculatedRoutes: Route[]) => {
// If the route calculation is finished, we don't have a progress listener anymore.
routingHandler = null;
if (err === GemError.success) {
const routesMap = map?.preferences.routes;
// Display the routes on map
calculatedRoutes.forEach((route, index) => {
// Add route with label
const label = getRouteLabel(route);
routesMap?.add(route, index === 0, { label });
});
// Center the camera on routes
map?.centerOnRoutes({ routes: calculatedRoutes });
showMessage('Route calculated successfully!');
} else {
showMessage('Route calculation failed.');
}
areRoutesBuilt = true;
updateUI();
}
);
updateUI();
}
// Method for starting the simulation and following the position
function startSimulation() {
if (!map) return;
const routes = map.preferences.routes;
// Set main route (using second route if available)
const routeAt1 = routes.at(1);
if (routeAt1) {
routes.mainRoute = routeAt1;
}
if (!routes.mainRoute) {
showMessage("No main route available");
return;
}
navigationHandler = NavigationService.startSimulation(
routes.mainRoute,
undefined,
{
onNavigationInstruction: (instruction: NavigationInstruction, events: any) => {
isSimulationActive = true;
currentInstruction = instruction;
updateNavigationUI();
updateUI();
},
onBetterRouteDetected: (route: Route, travelTime: number, delay: number, timeGain: number) => {
// Display notification when a better route is detected
showBetterRouteNotification(travelTime, delay, timeGain);
},
onBetterRouteInvalidated: () => {
console.log("The previously found better route is no longer valid");
},
onBetterRouteRejected: (reason: GemError) => {
console.log("The check for better route failed with reason:", reason);
},
onError: (error: GemError) => {
// If the navigation has ended or if an error occurred while navigating, remove routes
isSimulationActive = false;
cancelRoute();
if (error !== GemError.cancel) {
stopSimulation();
}
updateUI();
}
}
);
// Clear route alternatives from map
map.preferences.routes.clearAllButMainRoute();
// Set the camera to follow position
map.startFollowingPosition();
}
// Method for removing the routes from display
function cancelRoute() {
if (!map) return;
// Remove the routes from map
map.preferences.routes.clear();
if (routingHandler !== null) {
// Cancel the navigation
RoutingService.cancelRoute(routingHandler);
routingHandler = null;
}
areRoutesBuilt = false;
updateUI();
}
// Method to stop the simulation and remove the displayed routes
function stopSimulation() {
if (navigationHandler !== null) {
// Cancel the navigation
NavigationService.cancelNavigation(navigationHandler);
navigationHandler = null;
}
cancelRoute();
isSimulationActive = false;
updateUI();
}
Live Demo
Better Route Notification Modal
The modal displays when a better route is detected during navigation, showing travel time, delay, and time gain information.
// Show better route notification modal
function showBetterRouteNotification(travelTime: number, delay: number, timeGain: number) {
// Create modal overlay
const modal = document.createElement('div');
modal.style.cssText = `
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0, 0, 0, 0.5); z-index: 3000;
display: flex; justify-content: center; align-items: flex-end;
`;
// Create better route panel
const panel = document.createElement('div');
panel.style.cssText = `
background: white; border-radius: 20px 20px 0 0; padding: 24px;
width: 100%; max-width: 400px; margin: 20px;
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.3);
`;
panel.innerHTML = `
<div style="text-align: center; margin-bottom: 20px;">
<h3 style="margin: 0 0 10px 0; color: #333;">Better Route Found!</h3>
<div style="color: #666; font-size: 14px;">
<div>Travel Time: ${formatDuration(travelTime)}</div>
<div>Delay: ${formatDuration(delay)}</div>
<div style="color: #4CAF50; font-weight: bold;">Time Saved: ${formatDuration(timeGain)}</div>
</div>
</div>
<button id="dismiss-better-route" style="
width: 100%; padding: 12px; background: #673ab7; color: white;
border: none; border-radius: 8px; font-size: 16px; cursor: pointer;
">Dismiss</button>
`;
modal.appendChild(panel);
document.body.appendChild(modal);
// Add dismiss functionality
const dismissBtn = panel.querySelector('#dismiss-better-route') as HTMLButtonElement;
dismissBtn.onclick = () => {
document.body.removeChild(modal);
};
// Allow dismissing by clicking overlay
modal.onclick = (e) => {
if (e.target === modal) {
document.body.removeChild(modal);
}
};
}
Navigation UI Updates
Update navigation panels with current instruction information during simulation.
// Update navigation UI with current instruction
function updateNavigationUI() {
if (!currentInstruction) return;
// Update instruction panel
const instructionText = instructionPanel.querySelector('.instruction-text');
if (instructionText) {
instructionText.textContent = currentInstruction.nextTurnInstruction || 'Continue straight';
}
const instructionDistance = instructionPanel.querySelector('.instruction-distance');
if (instructionDistance) {
const timeDistance = currentInstruction.timeDistanceToNextTurn;
const distance = timeDistance.unrestrictedDistanceM + timeDistance.restrictedDistanceM;
instructionDistance.textContent = convertDistance(distance);
}
// Update bottom panel
const remainingDistance = bottomPanel.querySelector('.remaining-distance');
if (remainingDistance) {
const timeDistance = currentInstruction.remainingTravelTimeDistance;
const distance = timeDistance.unrestrictedDistanceM + timeDistance.restrictedDistanceM;
remainingDistance.textContent = convertDistance(distance);
}
const eta = bottomPanel.querySelector('.eta');
if (eta) {
// Calculate ETA based on remaining time
const timeDistance = currentInstruction.remainingTravelTimeDistance;
const remainingTimeS = timeDistance.unrestrictedTimeS + timeDistance.restrictedTimeS;
const now = new Date();
const etaTime = new Date(now.getTime() + remainingTimeS * 1000);
eta.textContent = etaTime.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
const remainingDuration = bottomPanel.querySelector('.remaining-duration');
if (remainingDuration) {
const timeDistance = currentInstruction.remainingTravelTimeDistance;
const duration = timeDistance.unrestrictedTimeS + timeDistance.restrictedTimeS;
remainingDuration.textContent = convertDuration(duration);
}
}
Utility Functions
// Utility: show a temporary message
function showMessage(message: string, duration = 3000) {
let msgDiv = document.getElementById('status-msg');
if (!msgDiv) {
msgDiv = document.createElement('div');
msgDiv.id = 'status-msg';
msgDiv.style.cssText = `
position: fixed; top: 20px; left: 50%; transform: translateX(-50%);
background: #333; color: #fff; padding: 12px 20px; border-radius: 8px;
z-index: 2000; font-size: 1em;
`;
document.body.appendChild(msgDiv);
}
msgDiv.textContent = message;
setTimeout(() => { msgDiv.textContent = ''; }, duration);
}
// Format duration from seconds to readable format
function formatDuration(seconds: number): string {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (hours > 0) {
return `${hours}h ${minutes}m`;
} else {
return `${minutes}m`;
}
}
// Utility function to get route label (distance and duration)
function getRouteLabel(route: Route): string {
const timeDistance = route.getTimeDistance();
const totalDistance = timeDistance.unrestrictedDistanceM + timeDistance.restrictedDistanceM;
const totalDuration = timeDistance.unrestrictedTimeS + timeDistance.restrictedTimeS;
return `${convertDistance(totalDistance)}\n${convertDuration(totalDuration)}`;
}
// Utility function to convert meters distance into a suitable format
function convertDistance(meters: number): string {
if (meters >= 1000) {
const kilometers = meters / 1000;
return `${kilometers.toFixed(1)} km`;
} else {
return `${meters.toString()} m`;
}
}
// Utility function to convert seconds duration into a suitable format
function convertDuration(seconds: number): string {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const hoursText = hours > 0 ? `${hours} h ` : '';
const minutesText = `${minutes} min`;
return hoursText + minutesText;
}