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

@@ -1,6 +1,7 @@
import { html, css, cssManager } from '@design.estate/dees-element';
import '@design.estate/dees-wcctools/demotools';
import type { DeesGeoMap } from './dees-geo-map.js';
import { getSpeedPresets, type TSimulationSpeed } from './geo-map.mock-gps.js';
export const demoFunc = () => html`
<style>
@@ -87,11 +88,42 @@ export const demoFunc = () => html`
transition: all 0.15s ease;
}
.control-button:hover {
.control-button:hover:not(:disabled) {
background: ${cssManager.bdTheme('#f0f0f0', '#333')};
border-color: ${cssManager.bdTheme('#999', '#666')};
}
.control-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.control-button.active {
background: ${cssManager.bdTheme('#0066cc', '#0084ff')};
color: #fff;
border-color: transparent;
}
.control-button.primary {
background: ${cssManager.bdTheme('#0066cc', '#0084ff')};
color: #fff;
border-color: transparent;
}
.control-button.primary:hover:not(:disabled) {
background: ${cssManager.bdTheme('#0055aa', '#0073e6')};
}
.control-button.danger {
background: ${cssManager.bdTheme('#dc2626', '#ef4444')};
color: #fff;
border-color: transparent;
}
.control-button.danger:hover:not(:disabled) {
background: ${cssManager.bdTheme('#b91c1c', '#dc2626')};
}
.feature-display {
background: ${cssManager.bdTheme('#f9f9f9', '#1e1e1e')};
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
@@ -132,6 +164,137 @@ export const demoFunc = () => html`
color: #fff;
border-color: transparent;
}
/* Simulation Panel Styles */
.simulation-section {
background: ${cssManager.bdTheme('#f0f9ff', '#1a2633')};
border: 1px solid ${cssManager.bdTheme('#bae6fd', '#1e3a5f')};
border-radius: 12px;
padding: 20px;
}
.simulation-controls-grid {
display: grid;
grid-template-columns: auto 1fr auto;
gap: 16px;
align-items: center;
}
.sim-playback {
display: flex;
gap: 8px;
}
.sim-speed {
display: flex;
align-items: center;
gap: 8px;
}
.sim-speed-label {
font-size: 12px;
color: ${cssManager.bdTheme('#666', '#999')};
white-space: nowrap;
}
.sim-speed-select {
padding: 8px 12px;
border: 1px solid ${cssManager.bdTheme('#ccc', '#444')};
border-radius: 6px;
background: ${cssManager.bdTheme('#fff', '#2a2a2a')};
color: ${cssManager.bdTheme('#333', '#fff')};
font-size: 13px;
cursor: pointer;
min-width: 150px;
}
.sim-voice {
display: flex;
align-items: center;
gap: 8px;
}
.sim-voice-checkbox {
width: 18px;
height: 18px;
cursor: pointer;
}
.sim-voice-label {
font-size: 13px;
color: ${cssManager.bdTheme('#333', '#fff')};
cursor: pointer;
user-select: none;
}
.sim-progress {
margin-top: 16px;
}
.sim-progress-header {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
}
.sim-progress-label {
font-size: 12px;
color: ${cssManager.bdTheme('#666', '#999')};
}
.sim-progress-value {
font-size: 12px;
font-weight: 600;
color: ${cssManager.bdTheme('#333', '#fff')};
}
.sim-progress-bar {
width: 100%;
height: 8px;
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')};
border-radius: 4px;
overflow: hidden;
}
.sim-progress-fill {
height: 100%;
background: ${cssManager.bdTheme('#0066cc', '#0084ff')};
border-radius: 4px;
transition: width 0.3s ease;
}
.sim-status {
margin-top: 12px;
padding: 8px 12px;
border-radius: 6px;
font-size: 12px;
text-align: center;
}
.sim-status.idle {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.05)')};
color: ${cssManager.bdTheme('#666', '#999')};
}
.sim-status.running {
background: rgba(34, 197, 94, 0.15);
color: ${cssManager.bdTheme('#166534', '#86efac')};
}
.sim-status.paused {
background: rgba(245, 158, 11, 0.15);
color: ${cssManager.bdTheme('#92400e', '#fcd34d')};
}
.sim-status.completed {
background: rgba(59, 130, 246, 0.15);
color: ${cssManager.bdTheme('#1e40af', '#93c5fd')};
}
.sim-status.no-route {
background: rgba(239, 68, 68, 0.15);
color: ${cssManager.bdTheme('#991b1b', '#fca5a5')};
}
`}
</style>
@@ -141,6 +304,59 @@ export const demoFunc = () => html`
const eventLog = elementArg.querySelector('#event-log') as HTMLElement;
const featureJson = elementArg.querySelector('#feature-json') as HTMLElement;
// Simulation state
let simulationState = {
isRunning: false,
isPaused: false,
progress: 0,
hasRoute: false,
};
const updateSimulationUI = () => {
const startBtn = elementArg.querySelector('#sim-start') as HTMLButtonElement;
const pauseBtn = elementArg.querySelector('#sim-pause') as HTMLButtonElement;
const stopBtn = elementArg.querySelector('#sim-stop') as HTMLButtonElement;
const progressFill = elementArg.querySelector('#sim-progress-fill') as HTMLElement;
const progressValue = elementArg.querySelector('#sim-progress-value') as HTMLElement;
const statusEl = elementArg.querySelector('#sim-status') as HTMLElement;
if (!startBtn) return;
// Update button states
startBtn.disabled = !simulationState.hasRoute || (simulationState.isRunning && !simulationState.isPaused);
pauseBtn.disabled = !simulationState.isRunning || simulationState.isPaused;
stopBtn.disabled = !simulationState.isRunning && !simulationState.isPaused;
// Update progress
if (progressFill) {
progressFill.style.width = `${simulationState.progress}%`;
}
if (progressValue) {
progressValue.textContent = `${simulationState.progress.toFixed(1)}%`;
}
// Update status
if (statusEl) {
statusEl.className = 'sim-status';
if (!simulationState.hasRoute) {
statusEl.className += ' no-route';
statusEl.textContent = 'Calculate a route first to enable simulation';
} else if (simulationState.isRunning && !simulationState.isPaused) {
statusEl.className += ' running';
statusEl.textContent = 'Simulation running...';
} else if (simulationState.isPaused) {
statusEl.className += ' paused';
statusEl.textContent = 'Simulation paused';
} else if (simulationState.progress >= 100) {
statusEl.className += ' completed';
statusEl.textContent = 'Simulation completed!';
} else {
statusEl.className += ' idle';
statusEl.textContent = 'Ready to simulate';
}
}
};
const addLogEntry = (type: string, message: string) => {
const entry = document.createElement('div');
entry.className = 'event-entry';
@@ -157,6 +373,7 @@ export const demoFunc = () => html`
if (map) {
map.addEventListener('map-ready', () => {
addLogEntry('ready', 'Map initialized successfully');
updateSimulationUI();
});
map.addEventListener('draw-change', (e: CustomEvent) => {
@@ -183,9 +400,106 @@ export const demoFunc = () => html`
const durationMin = Math.round(route.duration / 60);
addLogEntry('route', `${mode}: ${distKm} km, ${durationMin} min`);
console.log('Route calculated:', e.detail);
// Update simulation state
simulationState.hasRoute = true;
simulationState.progress = 0;
updateSimulationUI();
// Create/update the simulator with the new route
const simulator = map.createMockGPSSimulator();
simulator.setRoute(route);
});
map.addEventListener('guidance-event', (e: CustomEvent) => {
const event = e.detail;
addLogEntry('guidance', `${event.type}: ${event.instruction || 'Step ' + event.stepIndex}`);
console.log('Guidance event:', event);
// Update progress
const simulator = map.getMockGPSSimulator();
if (simulator) {
simulationState.progress = simulator.getProgress();
updateSimulationUI();
}
});
}
// Set up simulation controls
const setupSimulationControls = () => {
const startBtn = elementArg.querySelector('#sim-start') as HTMLButtonElement;
const pauseBtn = elementArg.querySelector('#sim-pause') as HTMLButtonElement;
const stopBtn = elementArg.querySelector('#sim-stop') as HTMLButtonElement;
const speedSelect = elementArg.querySelector('#sim-speed') as HTMLSelectElement;
const voiceCheckbox = elementArg.querySelector('#sim-voice') as HTMLInputElement;
if (startBtn && map) {
startBtn.addEventListener('click', () => {
let simulator = map.getMockGPSSimulator();
if (!simulator) {
simulator = map.createMockGPSSimulator();
const route = map.getNavigationState()?.route;
if (route) {
simulator.setRoute(route);
}
}
// Start guidance
map.startGuidance();
simulator.start();
simulationState.isRunning = true;
simulationState.isPaused = false;
updateSimulationUI();
addLogEntry('simulation', 'Started');
});
}
if (pauseBtn && map) {
pauseBtn.addEventListener('click', () => {
const simulator = map.getMockGPSSimulator();
if (simulator) {
simulator.pause();
simulationState.isPaused = true;
updateSimulationUI();
addLogEntry('simulation', 'Paused');
}
});
}
if (stopBtn && map) {
stopBtn.addEventListener('click', () => {
map.stopGuidance();
simulationState.isRunning = false;
simulationState.isPaused = false;
simulationState.progress = 0;
updateSimulationUI();
addLogEntry('simulation', 'Stopped');
});
}
if (speedSelect && map) {
speedSelect.addEventListener('change', () => {
const speed = speedSelect.value as TSimulationSpeed;
const simulator = map.getMockGPSSimulator();
if (simulator) {
simulator.setSpeed(speed);
addLogEntry('simulation', `Speed changed to ${speedSelect.options[speedSelect.selectedIndex].text}`);
}
});
}
if (voiceCheckbox && map) {
// Set initial state
voiceCheckbox.checked = true;
voiceCheckbox.addEventListener('change', () => {
map.setVoiceEnabled(voiceCheckbox.checked);
addLogEntry('simulation', `Voice ${voiceCheckbox.checked ? 'enabled' : 'disabled'}`);
});
}
};
// Set up navigation buttons
const locations: Record<string, [number, number]> = {
paris: [2.3522, 48.8566],
@@ -257,6 +571,12 @@ export const demoFunc = () => html`
updateFeatureDisplay();
});
}
// Initialize simulation controls after a tick
setTimeout(() => {
setupSimulationControls();
updateSimulationUI();
}, 100);
}}>
<div class="demo-section">
<h2 class="demo-title">Interactive Map with Drawing Tools</h2>
@@ -281,6 +601,58 @@ export const demoFunc = () => html`
</p>
</div>
<div class="demo-section simulation-section">
<h2 class="demo-title">GPS Simulation & Voice Navigation</h2>
<p class="demo-description">
Calculate a route using the navigation panel, then use these controls to simulate GPS movement
along the route with voice-guided turn-by-turn instructions.
</p>
<div class="simulation-controls-grid">
<div class="sim-playback">
<button class="control-button primary" id="sim-start" disabled>
▶ Start
</button>
<button class="control-button" id="sim-pause" disabled>
⏸ Pause
</button>
<button class="control-button danger" id="sim-stop" disabled>
⏹ Stop
</button>
</div>
<div class="sim-speed">
<span class="sim-speed-label">Speed:</span>
<select class="sim-speed-select" id="sim-speed">
${getSpeedPresets().map(preset => html`
<option value="${preset.id}" ?selected=${preset.id === 'city'}>
${preset.name} (${preset.kmh} km/h)
</option>
`)}
</select>
</div>
<div class="sim-voice">
<input type="checkbox" id="sim-voice" class="sim-voice-checkbox" checked />
<label for="sim-voice" class="sim-voice-label">Voice Guidance</label>
</div>
</div>
<div class="sim-progress">
<div class="sim-progress-header">
<span class="sim-progress-label">Route Progress</span>
<span class="sim-progress-value" id="sim-progress-value">0%</span>
</div>
<div class="sim-progress-bar">
<div class="sim-progress-fill" id="sim-progress-fill" style="width: 0%"></div>
</div>
</div>
<div class="sim-status no-route" id="sim-status">
Calculate a route first to enable simulation
</div>
</div>
<div class="demo-section">
<h2 class="demo-title">Quick Navigation</h2>
<div class="locations-grid">