feat(dees-geo-map): Highlight current navigation step with progress, mark completed steps, auto-scroll turn-by-turn list, expose guidance state for synchronization, and refine instruction/voice wording
This commit is contained in:
@@ -3,6 +3,7 @@ import maplibregl from 'maplibre-gl';
|
||||
import { renderIcon } from './geo-map.icons.js';
|
||||
import { type INominatimResult } from './geo-map.search.js';
|
||||
import type { ITrafficAwareRoute } from './geo-map.traffic.providers.js';
|
||||
import type { INavigationGuideState } from './geo-map.navigation-guide.js';
|
||||
|
||||
// ─── Navigation/Routing Types ────────────────────────────────────────────────
|
||||
|
||||
@@ -29,6 +30,7 @@ export interface IOSRMStep {
|
||||
location: [number, number];
|
||||
};
|
||||
name: string; // Street name
|
||||
ref?: string; // Road reference (A1, M25, etc.)
|
||||
distance: number;
|
||||
duration: number;
|
||||
driving_side: string;
|
||||
@@ -69,6 +71,8 @@ export interface INavigationControllerCallbacks {
|
||||
onTrafficToggle?: (enabled: boolean) => void;
|
||||
/** Optional callback to get current traffic state */
|
||||
getTrafficEnabled?: () => boolean;
|
||||
/** Optional callback to get current guidance state for turn-by-turn synchronization */
|
||||
getGuidanceState?: () => INavigationGuideState | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -742,7 +746,7 @@ export class NavigationController {
|
||||
*/
|
||||
public formatStepInstruction(step: IOSRMStep): string {
|
||||
const { type, modifier } = step.maneuver;
|
||||
const name = step.name || 'unnamed road';
|
||||
const name = step.name || step.ref || 'unnamed road';
|
||||
|
||||
switch (type) {
|
||||
case 'depart':
|
||||
@@ -767,7 +771,7 @@ export class NavigationController {
|
||||
case 'end of road':
|
||||
return `At the end of the road, turn ${modifier || ''} onto ${name}`;
|
||||
case 'new name':
|
||||
return `Continue onto ${name}`;
|
||||
return `Continue on ${name}`;
|
||||
default:
|
||||
return `${type} ${modifier || ''} on ${name}`.trim();
|
||||
}
|
||||
@@ -1044,20 +1048,44 @@ export class NavigationController {
|
||||
}
|
||||
|
||||
const steps = route.legs.flatMap(leg => leg.steps);
|
||||
const guidanceState = this.callbacks.getGuidanceState?.();
|
||||
const isNavigating = guidanceState?.isNavigating ?? false;
|
||||
const currentStepIndex = guidanceState?.currentStepIndex ?? -1;
|
||||
|
||||
if (steps.length === 0) {
|
||||
return html`<div class="nav-empty-steps">No turn-by-turn directions available</div>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
${steps.map(step => {
|
||||
${steps.map((step, index) => {
|
||||
const icon = this.getManeuverIcon(step.maneuver.type, step.maneuver.modifier);
|
||||
const instruction = this.formatStepInstruction(step);
|
||||
const distance = this.formatDistance(step.distance);
|
||||
|
||||
// currentStepIndex points to the maneuver we're approaching,
|
||||
// but we're traveling on the PREVIOUS step's road
|
||||
const isCurrent = isNavigating && index === currentStepIndex - 1;
|
||||
const isCompleted = isNavigating && index < currentStepIndex - 1;
|
||||
|
||||
// Calculate progress percentage for current step
|
||||
let progressPercent = 0;
|
||||
if (isCurrent && step.distance > 0) {
|
||||
const distanceRemaining = guidanceState?.distanceToNextManeuver ?? step.distance;
|
||||
progressPercent = Math.max(0, Math.min(100,
|
||||
((step.distance - distanceRemaining) / step.distance) * 100
|
||||
));
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="nav-step" @click=${() => this.flyToStep(step)}>
|
||||
<div class="nav-step-icon">${icon}</div>
|
||||
<div
|
||||
class="nav-step ${isCurrent ? 'current' : ''} ${isCompleted ? 'completed' : ''}"
|
||||
@click=${() => this.flyToStep(step)}
|
||||
data-step-index="${index}"
|
||||
>
|
||||
${isCurrent ? html`
|
||||
<div class="nav-step-progress-bar" style="width: ${progressPercent}%"></div>
|
||||
` : ''}
|
||||
<div class="nav-step-icon">${isCompleted ? '✓' : icon}</div>
|
||||
<div class="nav-step-content">
|
||||
<div class="nav-step-instruction">${instruction}</div>
|
||||
<div class="nav-step-distance">${distance}</div>
|
||||
@@ -1067,4 +1095,26 @@ export class NavigationController {
|
||||
})}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll the turn-by-turn list to show the current step
|
||||
* Called externally when guidance state changes
|
||||
*/
|
||||
public scrollToCurrentStep(stepIndex: number): void {
|
||||
// Use requestAnimationFrame to ensure DOM is updated
|
||||
requestAnimationFrame(() => {
|
||||
// Find elements in document - they may be in shadow DOM
|
||||
const stepsContainer = document.querySelector('.nav-steps')
|
||||
?? document.querySelector('dees-geo-map')?.shadowRoot?.querySelector('.nav-steps');
|
||||
const currentStep = document.querySelector(`.nav-step[data-step-index="${stepIndex}"]`)
|
||||
?? document.querySelector('dees-geo-map')?.shadowRoot?.querySelector(`.nav-step[data-step-index="${stepIndex}"]`);
|
||||
|
||||
if (stepsContainer && currentStep) {
|
||||
(currentStep as HTMLElement).scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'nearest',
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user