Skip to main content

Speed TTS Warning

|

This example demonstrates how to integrate Text-to-Speech (TTS) voice warnings for speed limit changes during navigation using the Maps SDK for TypeScript with browser TTS capabilities.

Live Demo

Overview

The example showcases the following features:

  • Route calculation between two points
  • Navigation simulation with position following
  • Speed limit detection using AlarmService
  • Real-time TTS announcements for speed limit changes
  • Visual display of current speed limits
  • Integration with browser's window.speechSynthesis API

Code Implementation

Initialize GemKit and setup UI

index.ts
import {
GemKit,
GemMap,
Landmark,
RoutePreferences,
RoutingService,
GemError,
Route,
NavigationService,
NavigationInstruction,
PositionService,
AlarmService,
AlarmListener,
Coordinates,
TaskHandler
} from '@magiclane/maps-sdk';
import { GEMKIT_TOKEN } from './token';

let map: GemMap | null = null;
let ttsEngine: TTSEngine;
let areRoutesBuilt = false;
let isSimulationActive = false;
let routingHandler: TaskHandler | null = null;
let navigationHandler: TaskHandler | null = null;
let alarmService: AlarmService | null = null;
let alarmListener: AlarmListener | null = null;
let currentSpeedLimit: number | null = null;

// UI References
let controlsDiv: HTMLDivElement;
let buildRouteBtn: HTMLButtonElement;
let startSimBtn: HTMLButtonElement;
let stopSimBtn: HTMLButtonElement;
let speedLimitPanel: HTMLDivElement;
let followBtn: HTMLButtonElement | null = null;

window.addEventListener('DOMContentLoaded', async () => {
const gemKit = await GemKit.initialize(GEMKIT_TOKEN);
ttsEngine = new TTSEngine();

const mapContainer = document.getElementById('map-container');
if (!mapContainer) throw new Error('Map container not found');

const viewId = 2;
const wrapper = gemKit.createView(viewId, (gemMap: GemMap) => {
map = gemMap;
routingHandler = null;
areRoutesBuilt = false;
updateUI();
});
if (wrapper) mapContainer.appendChild(wrapper);

// --- Controls Container (Fixed Header) ---
controlsDiv = document.createElement('div');
controlsDiv.style.cssText = `
position: fixed; top: 30px; left: 50%; transform: translateX(-50%);
display: flex; gap: 12px; z-index: 2000; align-items: center; justify-content: center;
`;
document.body.appendChild(controlsDiv);

// Build Route button
buildRouteBtn = document.createElement('button');
buildRouteBtn.innerHTML = `${ICONS.route} Build Route`;
styleButton(buildRouteBtn, '#673ab7', '#7e57c2');
buildRouteBtn.onclick = () => onBuildRouteButtonPressed();
controlsDiv.appendChild(buildRouteBtn);

// Start Simulation button
startSimBtn = document.createElement('button');
startSimBtn.innerHTML = `${ICONS.play} Start Simulation`;
styleButton(startSimBtn, '#4caf50', '#66bb6a');
startSimBtn.onclick = () => startSimulation();
controlsDiv.appendChild(startSimBtn);

// Stop Simulation button
stopSimBtn = document.createElement('button');
stopSimBtn.innerHTML = `${ICONS.stop} Stop Simulation`;
styleButton(stopSimBtn, '#f44336', '#ef5350');
stopSimBtn.onclick = () => stopSimulation();
controlsDiv.appendChild(stopSimBtn);

// Initialize Speed Limit Panel (Hidden by default)
createSpeedLimitPanel();

updateUI();
});

TTS Engine Implementation

index.ts
class TTSEngine {
private synth: SpeechSynthesis;
private utter: SpeechSynthesisUtterance | null = null;

constructor() {
this.synth = window.speechSynthesis;
}

speakText(text: string) {
if (this.synth.speaking) {
this.synth.cancel();
}
this.utter = new SpeechSynthesisUtterance(text);
this.utter.rate = 0.9;
this.utter.pitch = 1.0;
this.utter.volume = 1.0;
this.synth.speak(this.utter);
}

stop() {
if (this.synth.speaking) {
this.synth.cancel();
}
}
}

Calculate Route

index.ts
function onBuildRouteButtonPressed() {
// Departure: Kassel
const departureLandmark = Landmark.withCoordinates(
Coordinates.fromLatLong(51.35416637819253, 9.378580176120199)
);
// Destination: Kassel (nearby)
const destinationLandmark = Landmark.withCoordinates(
Coordinates.fromLatLong(51.36704970265849, 9.404698019844462)
);

const routePreferences = new RoutePreferences({});
showMessage('Calculating route...');

routingHandler = RoutingService.calculateRoute(
[departureLandmark, destinationLandmark],
routePreferences,
(err: GemError, routes: Route[]) => {
routingHandler = null;
if (err === GemError.success && routes.length > 0) {
const routesMap = map!.preferences.routes;
routes.forEach((route: Route, idx: number) => {
routesMap.add(route, idx === 0);
});
map!.centerOnRoutes({ routes });
areRoutesBuilt = true;
showMessage('Route built successfully.');
} else {
showMessage('Route calculation failed.');
}
updateUI();
}
);
updateUI();
}
routingHandler = null;

if (err === GemError.success && routes.length > 0) {
const routesMap = map!.preferences.routes;
routes.forEach((route: Route, idx: number) => {
routesMap.add(route, idx === 0);
});
map!.centerOnRoutes({ routes });
areRoutesBuilt = true;
} else {
showMessage('Route calculation failed.');
}

updateUI();
}
);

updateUI();
}

Start Simulation with Speed Alarms

index.ts
function startSimulation() {
const routes = map!.preferences.routes;
routes.clearAllButMainRoute?.();

if (!routes.mainRoute) {
showMessage('No main route available');
return;
}

alarmListener = AlarmListener.create({
onSpeedLimit: async (speed: number, limit: number, insideCityArea: boolean) => {
// API often returns m/s, convert to km/h
const speedLimitConverted = Math.round(limit * 3.6);

if (currentSpeedLimit !== speedLimitConverted) {
currentSpeedLimit = speedLimitConverted;
updateUI();

const speedWarning = `Speed limit is ${speedLimitConverted} kilometers per hour`;
ttsEngine.speakText(speedWarning);
showMessage(`Alert: ${speedLimitConverted} km/h limit`);
}
},
});

alarmService = AlarmService.create(alarmListener);

// Start navigation simulation
navigationHandler = NavigationService.startSimulation(
routes.mainRoute,
undefined,
{
onNavigationInstruction: (instruction: NavigationInstruction) => {
isSimulationActive = true;
updateUI();
},
onDestinationReached: (landmark: any) => {
stopSimulation();
cancelRoute();
},
onError: (error: GemError) => {
isSimulationActive = false;
cancelRoute();
if (error !== GemError.cancel) {
stopSimulation();
}
}
}
);

// Enable position following
map!.startFollowingPosition?.();
isSimulationActive = true;
updateUI();
}

Stop Simulation

index.ts
function stopSimulation() {
if (navigationHandler) {
NavigationService.cancelNavigation(navigationHandler);
navigationHandler = null;
}

cancelRoute();
isSimulationActive = false;
currentSpeedLimit = null;
alarmService = null;
ttsEngine.stop();

updateUI();
}

function cancelRoute() {
map!.preferences.routes.clear?.();

if (routingHandler) {
RoutingService.cancelRoute(routingHandler);
routingHandler = null;
}

areRoutesBuilt = false;
updateUI();
}

Visual Speed Display

index.ts
function createSpeedLimitPanel() {
speedLimitPanel = document.createElement('div');
speedLimitPanel.style.cssText = `
position: fixed;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.9);
color: #fff;
border: 3px solid #f44336;
border-radius: 50%;
width: 100px;
height: 100px;
display: none;
justify-content: center;
align-items: center;
font-size: 24px;
font-weight: bold;
z-index: 2001;
`;
document.body.appendChild(speedLimitPanel);
}

function updateSpeedPanel(limit: number) {
if (speedLimitPanel) {
speedLimitPanel.textContent = `${limit}`;
speedLimitPanel.style.display = 'flex';
}
}

function updateUI() {
if (buildRouteBtn) {
buildRouteBtn.style.display = !routingHandler && !areRoutesBuilt ? 'block' : 'none';
}
if (startSimBtn) {
startSimBtn.style.display = !isSimulationActive && areRoutesBuilt ? 'block' : 'none';
}
if (stopSimBtn) {
stopSimBtn.style.display = isSimulationActive ? 'block' : 'none';
}

if (currentSpeedLimit !== null) {
updateSpeedPanel(currentSpeedLimit);
} else if (speedLimitPanel) {
speedLimitPanel.style.display = 'none';
}
}

Key Features

  • Browser TTS Integration: Uses window.speechSynthesis for voice announcements
  • Speed Limit Detection: Real-time monitoring via AlarmService.onSpeedLimit
  • Automatic Conversion: Converts speed from m/s to km/h for display
  • Change Detection: Only announces when speed limit changes
  • Visual Feedback: Bottom panel displays current speed limit
  • Position Following: Camera follows simulated position during navigation
  • Clean State Management: Proper cleanup when stopping simulation

Alarm Service

The AlarmService provides real-time notifications during navigation:

alarmListener = AlarmListener.create({
onSpeedLimit: async (speed: number, limit: number, insideCityArea: boolean) => {
// speed: Current vehicle speed (m/s)
// limit: Speed limit at current location (m/s)
// insideCityArea: Whether inside city boundaries
}
});

alarmService = AlarmService.create(alarmListener);

TTS Configuration

Customize voice characteristics through SpeechSynthesisUtterance:

utterance.rate = 0.75;    // Speed: 0.1 to 10 (1 = normal)
utterance.pitch = 1.0; // Pitch: 0 to 2 (1 = normal)
utterance.volume = 0.8; // Volume: 0 to 1 (1 = maximum)
utterance.lang = 'en-US'; // Language code

Speed Conversion

Convert between m/s (SDK format) and km/h (display format):

// m/s to km/h
const speedKmh = Math.round(speedMs * 3.6);

// km/h to m/s
const speedMs = speedKmh / 3.6;

Workflow

  1. Build Route: Calculate route between departure and destination
  2. Start Simulation: Begin simulated navigation with alarm monitoring
  3. Speed Detection: AlarmService detects speed limit changes
  4. TTS Announcement: Browser TTS speaks the new speed limit
  5. Visual Update: Bottom panel displays current speed limit
  6. Stop Simulation: Clean up navigation and alarm services

Use Cases

Speed TTS warnings are useful for:

  • Driver Assistance: Alert drivers to changing speed limits
  • Navigation Safety: Prevent speeding violations
  • Educational Tools: Teaching road rules and speed awareness
  • Fleet Management: Monitor speed compliance during routes
  • Accessibility: Audio feedback for visually impaired users
  • Testing: Verify route speed limit data accuracy

Browser TTS Limitations

When using window.speechSynthesis:

  • Voice quality varies by browser and OS
  • Limited voice selection compared to native platforms
  • No human voice support (TTS only)
  • Utterances may be interrupted by page events
  • Some browsers require user interaction before TTS

Best Practices

  • Cancel Before Speaking: Always cancel ongoing speech before new announcements
  • Debounce Changes: Only announce when speed limit actually changes
  • Clear Speech: Use slightly slower rate (0.75) for better clarity
  • Error Handling: Check for synth.speaking before canceling
  • Cleanup: Stop TTS when navigation ends

Next Steps