From 89670ecad317c89635231992013006284ac5c7ee Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Thu, 5 Feb 2026 22:51:41 +0000 Subject: [PATCH] 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 --- changelog.md | 10 ++++ ts_web/00_commitinfo_data.ts | 2 +- ts_web/elements/00componentstyles.ts | 29 +++++++++ .../00group-map/dees-geo-map/dees-geo-map.ts | 7 +++ .../dees-geo-map/geo-map.navigation-guide.ts | 13 +++- .../dees-geo-map/geo-map.navigation.ts | 60 +++++++++++++++++-- .../00group-map/dees-geo-map/geo-map.voice.ts | 6 +- 7 files changed, 118 insertions(+), 9 deletions(-) diff --git a/changelog.md b/changelog.md index 70639a2..d63c164 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,15 @@ # Changelog +## 2026-02-05 - 1.5.0 - 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 + +- Add .nav-step.current and .nav-step.completed styles and a .nav-step-progress-bar to visually indicate current step and progress +- Expose getGuidanceState callback to read current navigation/guidance state for turn-by-turn synchronization +- Auto-scroll the turn-by-turn list when the active step changes and add scrollToCurrentStep(stepIndex) to perform smooth scrolling +- Render current/completed state in the step list (checkmark for completed) and calculate/display progress percent for the active step +- Use step.ref as a fallback for step.name and add handling/icons for the 'new name' maneuver +- Refine instruction formatting: use 'on' for continue maneuvers and 'onto' for turns/merges, and update voice output to reflect this + ## 2026-02-05 - 1.4.0 - feat(dees-geo-map) Add voice-guided navigation with TTS, navigation guide controller and mock GPS simulator diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index 42b6f4f..d492e8d 100644 --- a/ts_web/00_commitinfo_data.ts +++ b/ts_web/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@design.estate/dees-catalog-geo', - version: '1.4.0', + version: '1.5.0', description: 'A geospatial web components library with MapLibre GL JS maps and terra-draw drawing tools' } diff --git a/ts_web/elements/00componentstyles.ts b/ts_web/elements/00componentstyles.ts index 255827c..940fa22 100644 --- a/ts_web/elements/00componentstyles.ts +++ b/ts_web/elements/00componentstyles.ts @@ -754,6 +754,35 @@ export const navigationStyles = css` color: ${cssManager.bdTheme('rgba(0, 0, 0, 0.5)', 'rgba(255, 255, 255, 0.5)')}; } + /* Current step - highlighted during active navigation */ + .nav-step.current { + position: relative; + background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.1)', 'rgba(59, 130, 246, 0.15)')}; + border-left: 3px solid #3b82f6; + padding-left: 9px; + } + + /* Completed steps - muted appearance */ + .nav-step.completed { + opacity: 0.6; + } + + .nav-step.completed .nav-step-icon { + background: ${cssManager.bdTheme('rgba(34, 197, 94, 0.2)', 'rgba(34, 197, 94, 0.25)')}; + color: #22c55e; + } + + /* Progress bar on current step */ + .nav-step-progress-bar { + position: absolute; + bottom: 0; + left: 0; + height: 3px; + background: linear-gradient(90deg, #3b82f6 0%, #60a5fa 100%); + border-radius: 0 2px 2px 0; + transition: width 0.3s ease-out; + } + .nav-error { display: flex; align-items: center; diff --git a/ts_web/elements/00group-map/dees-geo-map/dees-geo-map.ts b/ts_web/elements/00group-map/dees-geo-map/dees-geo-map.ts index f1e5a51..644fbd3 100644 --- a/ts_web/elements/00group-map/dees-geo-map/dees-geo-map.ts +++ b/ts_web/elements/00group-map/dees-geo-map/dees-geo-map.ts @@ -348,6 +348,8 @@ export class DeesGeoMap extends DeesElement { this.showTraffic = enabled; }, getTrafficEnabled: () => this.showTraffic, + // Pass guidance state for turn-by-turn synchronization + getGuidanceState: () => this.guidanceController?.state ?? null, }); this.navigationController.navigationMode = this.navigationMode; @@ -375,6 +377,11 @@ export class DeesGeoMap extends DeesElement { bubbles: true, composed: true, })); + + // Auto-scroll turn-by-turn list when step changes + if (event.type === 'step-change' && this.navigationController) { + this.navigationController.scrollToCurrentStep(event.stepIndex); + } }, onRequestUpdate: () => this.requestUpdate(), getMap: () => this.map, diff --git a/ts_web/elements/00group-map/dees-geo-map/geo-map.navigation-guide.ts b/ts_web/elements/00group-map/dees-geo-map/geo-map.navigation-guide.ts index 9eaa9ab..18dd7fc 100644 --- a/ts_web/elements/00group-map/dees-geo-map/geo-map.navigation-guide.ts +++ b/ts_web/elements/00group-map/dees-geo-map/geo-map.navigation-guide.ts @@ -184,6 +184,8 @@ function formatManeuverForVoice(type: string, modifier?: string): string { return 'continue straight'; case 'end of road': return `at the end of the road, turn ${modifier || 'around'}`; + case 'new name': + return 'continue straight'; default: return type; } @@ -597,7 +599,9 @@ export class NavigationGuideController { // Approach - "In 200 meters, turn left" const distanceStr = formatDistanceForVoice(distance); this.voiceManager.speakApproach(distanceStr, maneuver, streetName); - const instruction = `In ${distanceStr}, ${maneuver}${streetName ? ` onto ${streetName}` : ''}`; + // Use "on" for continue, "onto" for turns/merges + const preposition = maneuver.startsWith('continue') ? 'on' : 'onto'; + const instruction = `In ${distanceStr}, ${maneuver}${streetName ? ` ${preposition} ${streetName}` : ''}`; this.emitGuidanceEvent('approach-maneuver', position, step, instruction); } } @@ -867,6 +871,7 @@ export class NavigationGuideController { 'merge': '⤵️', 'fork-left': '↖', 'fork-right': '↗', + 'new name': '⬆️', }; const key = modifier ? `${type}-${modifier}` : type; @@ -894,6 +899,12 @@ export class NavigationGuideController { case 'roundabout': case 'rotary': return `At the roundabout, take the exit onto ${name}`; + case 'fork': + return `Take the ${modifier || ''} fork onto ${name}`; + case 'end of road': + return `At the end of the road, turn ${modifier || ''} onto ${name}`; + case 'new name': + return `Continue on ${name}`; default: return `${type} on ${name}`; } diff --git a/ts_web/elements/00group-map/dees-geo-map/geo-map.navigation.ts b/ts_web/elements/00group-map/dees-geo-map/geo-map.navigation.ts index 66814df..3549111 100644 --- a/ts_web/elements/00group-map/dees-geo-map/geo-map.navigation.ts +++ b/ts_web/elements/00group-map/dees-geo-map/geo-map.navigation.ts @@ -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``; } 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` -