feat(dees-geo-map): Add voice-guided navigation with TTS, navigation guide controller and mock GPS simulator
This commit is contained in:
@@ -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 -->
|
||||
|
||||
Reference in New Issue
Block a user