feat(dees-geo-map): Add voice-guided navigation with TTS, navigation guide controller and mock GPS simulator

This commit is contained in:
2026-02-05 17:50:45 +00:00
parent 428e0546bd
commit 50b5c9325c
23 changed files with 2860 additions and 7 deletions

View File

@@ -11,7 +11,7 @@ import {
domtools,
} from '@design.estate/dees-element';
import { DeesContextmenu } from '@design.estate/dees-catalog';
import { geoComponentStyles, mapContainerStyles, toolbarStyles, searchStyles, navigationStyles, trafficStyles, headerToolbarStyles } from '../../00componentstyles.js';
import { geoComponentStyles, mapContainerStyles, toolbarStyles, searchStyles, navigationStyles, trafficStyles, headerToolbarStyles, guidanceStyles, maplibreMarkerStyles } from '../../00componentstyles.js';
// MapLibre imports
import maplibregl from 'maplibre-gl';
@@ -33,9 +33,12 @@ import { TerraDrawMapLibreGLAdapter } from 'terra-draw-maplibre-gl-adapter';
// Modular imports
import { renderIcon } from './geo-map.icons.js';
import { SearchController, type INominatimResult, type IAddressSelectedEvent } from './geo-map.search.js';
import { NavigationController, type TNavigationMode, type INavigationState, type IRouteCalculatedEvent } from './geo-map.navigation.js';
import { NavigationController, type TNavigationMode, type INavigationState, type IRouteCalculatedEvent, type IOSRMRoute } from './geo-map.navigation.js';
import { TrafficController } from './geo-map.traffic.js';
import { HereTrafficProvider, type ITrafficProvider } from './geo-map.traffic.providers.js';
import { NavigationGuideController, type IGPSPosition, type IGuidanceEvent, type INavigationGuideState, type INavigationCameraConfig } from './geo-map.navigation-guide.js';
import { MockGPSSimulator, type TSimulationSpeed, type IMockGPSConfig } from './geo-map.mock-gps.js';
import type { IVoiceConfig } from './geo-map.voice.js';
// Re-export types for external consumers
export type { INominatimResult, IAddressSelectedEvent } from './geo-map.search.js';
@@ -48,6 +51,9 @@ export type {
IRouteCalculatedEvent,
} from './geo-map.navigation.js';
export type { ITrafficProvider, ITrafficFlowData, ITrafficAwareRoute } from './geo-map.traffic.providers.js';
export type { IGPSPosition, IGuidanceEvent, INavigationGuideState, TGuidanceEventType, INavigationCameraConfig } from './geo-map.navigation-guide.js';
export type { IVoiceConfig } from './geo-map.voice.js';
export type { TSimulationSpeed, IMockGPSConfig } from './geo-map.mock-gps.js';
export type TDrawTool = 'polygon' | 'rectangle' | 'point' | 'linestring' | 'circle' | 'freehand' | 'select' | 'static';
@@ -128,6 +134,13 @@ export class DeesGeoMap extends DeesElement {
@property({ type: String })
accessor trafficApiKey: string = '';
// Guidance properties
@property({ type: Boolean })
accessor enableGuidance: boolean = false;
@property({ type: Object })
accessor voiceConfig: Partial<IVoiceConfig> = {};
// ─── State ──────────────────────────────────────────────────────────────────
@state()
@@ -149,6 +162,8 @@ export class DeesGeoMap extends DeesElement {
private searchController: SearchController | null = null;
private navigationController: NavigationController | null = null;
private trafficController: TrafficController | null = null;
private guidanceController: NavigationGuideController | null = null;
private mockGPSSimulator: MockGPSSimulator | null = null;
// ─── Styles ─────────────────────────────────────────────────────────────────
@@ -161,6 +176,8 @@ export class DeesGeoMap extends DeesElement {
navigationStyles,
trafficStyles,
headerToolbarStyles,
guidanceStyles,
maplibreMarkerStyles,
css`
:host {
display: block;
@@ -348,6 +365,22 @@ export class DeesGeoMap extends DeesElement {
hereProvider.configure({ apiKey: this.trafficApiKey });
this.trafficController.setProvider(hereProvider);
}
// Initialize guidance controller
this.guidanceController = new NavigationGuideController(
{
onGuidanceEvent: (event) => {
this.dispatchEvent(new CustomEvent<IGuidanceEvent>('guidance-event', {
detail: event,
bubbles: true,
composed: true,
}));
},
onRequestUpdate: () => this.requestUpdate(),
getMap: () => this.map,
},
this.voiceConfig
);
}
// ─── Map Initialization ─────────────────────────────────────────────────────
@@ -772,6 +805,199 @@ export class DeesGeoMap extends DeesElement {
return this.trafficController;
}
// ─── Guidance Public Methods ────────────────────────────────────────────────
/**
* Set current GPS position for navigation guidance
* @param coords - [lng, lat] coordinates
* @param heading - Optional heading in degrees (0 = North)
* @param speed - Optional speed in meters/second
*/
public setPosition(coords: [number, number], heading?: number, speed?: number): void {
this.guidanceController?.setPosition(coords[0], coords[1], heading, speed);
}
/**
* Start voice-guided navigation for the current route
*/
public startGuidance(): void {
const route = this.navigationController?.navigationState?.route;
if (route && this.guidanceController) {
this.guidanceController.startGuidance(route);
} else {
console.warn('[dees-geo-map] Cannot start guidance: no route calculated');
}
}
/**
* Stop voice-guided navigation
*/
public stopGuidance(): void {
this.guidanceController?.stopGuidance();
this.mockGPSSimulator?.stop();
}
/**
* Enable or disable voice guidance
*/
public setVoiceEnabled(enabled: boolean): void {
this.guidanceController?.setVoiceEnabled(enabled);
}
/**
* Check if voice guidance is enabled
*/
public isVoiceEnabled(): boolean {
return this.guidanceController?.isVoiceEnabled() ?? false;
}
/**
* Get current guidance state
*/
public getGuidanceState(): INavigationGuideState | null {
return this.guidanceController?.state ?? null;
}
/**
* Check if actively navigating
*/
public isNavigating(): boolean {
return this.guidanceController?.isNavigating() ?? false;
}
/**
* Create a mock GPS simulator for testing/demo
* The simulator emits position updates along the current route
*/
public createMockGPSSimulator(config?: Partial<IMockGPSConfig>): MockGPSSimulator {
// Clean up existing simulator
if (this.mockGPSSimulator) {
this.mockGPSSimulator.cleanup();
}
this.mockGPSSimulator = new MockGPSSimulator(
{
onPositionUpdate: (position: IGPSPosition) => {
if (this.guidanceController) {
this.guidanceController.updatePosition(position);
}
},
onSimulationStart: () => {
console.log('[MockGPSSimulator] Simulation started');
},
onSimulationPause: () => {
console.log('[MockGPSSimulator] Simulation paused');
},
onSimulationStop: () => {
console.log('[MockGPSSimulator] Simulation stopped');
},
onSimulationComplete: () => {
console.log('[MockGPSSimulator] Simulation complete');
},
},
config
);
// Set the route if available
const route = this.navigationController?.navigationState?.route;
if (route) {
this.mockGPSSimulator.setRoute(route);
}
return this.mockGPSSimulator;
}
/**
* Get the mock GPS simulator (if created)
*/
public getMockGPSSimulator(): MockGPSSimulator | null {
return this.mockGPSSimulator;
}
/**
* Get the guidance controller for advanced usage
*/
public getGuidanceController(): NavigationGuideController | null {
return this.guidanceController;
}
// ─── Navigation Camera Control Methods ─────────────────────────────────────
/**
* Enable or disable camera following the GPS position during navigation
* @param enabled - Whether the camera should follow the position
*/
public setNavigationFollowPosition(enabled: boolean): void {
this.guidanceController?.setFollowPosition(enabled);
}
/**
* Check if camera is following position during navigation
*/
public isNavigationFollowingPosition(): boolean {
return this.guidanceController?.isFollowingPosition() ?? true;
}
/**
* Enable or disable camera rotating with heading during navigation
* @param enabled - Whether the camera should rotate with heading
*/
public setNavigationFollowBearing(enabled: boolean): void {
this.guidanceController?.setFollowBearing(enabled);
}
/**
* Check if camera is following bearing during navigation
*/
public isNavigationFollowingBearing(): boolean {
return this.guidanceController?.isFollowingBearing() ?? true;
}
/**
* Set the navigation camera pitch (3D tilt angle)
* @param pitch - Angle in degrees (0 = flat, 60 = tilted for 3D view)
*/
public setNavigationPitch(pitch: number): void {
this.guidanceController?.setPitch(pitch);
}
/**
* Get current navigation pitch setting
*/
public getNavigationPitch(): number {
return this.guidanceController?.getPitch() ?? 60;
}
/**
* Set the navigation zoom level
* @param zoom - Zoom level (typically 15-19 for street-level navigation)
*/
public setNavigationZoom(zoom: number): void {
this.guidanceController?.setZoom(zoom);
}
/**
* Get current navigation zoom setting
*/
public getNavigationZoom(): number {
return this.guidanceController?.getZoom() ?? 17;
}
/**
* Get the full navigation camera configuration
*/
public getNavigationCameraConfig(): INavigationCameraConfig | null {
return this.guidanceController?.getCameraConfig() ?? null;
}
/**
* Set navigation camera configuration
* @param config - Partial camera configuration to apply
*/
public setNavigationCameraConfig(config: Partial<INavigationCameraConfig>): void {
this.guidanceController?.setCameraConfig(config);
}
// ─── Private Methods ────────────────────────────────────────────────────────
private ensureMaplibreCssLoaded() {
@@ -793,6 +1019,8 @@ export class DeesGeoMap extends DeesElement {
// Clean up controllers
this.navigationController?.cleanup();
this.trafficController?.cleanup();
this.guidanceController?.cleanup();
this.mockGPSSimulator?.cleanup();
if (this.map) {
this.map.remove();
@@ -923,6 +1151,9 @@ export class DeesGeoMap extends DeesElement {
<!-- Bottom Right: Empty (zoom in header) -->
<div class="overlay-bottom-right"></div>
</div>
<!-- Guidance Panel (shown during active navigation) -->
${this.guidanceController?.isNavigating() ? this.guidanceController.render() : ''}
</div>
<!-- Right Sidebar: Draw Panel -->