feat(geo-map): add live traffic visualization and traffic-aware routing with pluggable providers and UI integration

This commit is contained in:
2026-02-05 15:07:33 +00:00
parent 1a0fceadc0
commit df690dc329
22 changed files with 2238 additions and 181 deletions

View File

@@ -2,6 +2,7 @@ import { html, type TemplateResult } from '@design.estate/dees-element';
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';
// ─── Navigation/Routing Types ────────────────────────────────────────────────
@@ -39,6 +40,7 @@ export interface INavigationState {
startAddress: string;
endAddress: string;
route: IOSRMRoute | null;
trafficRoute: ITrafficAwareRoute | null;
isLoading: boolean;
error: string | null;
}
@@ -57,6 +59,12 @@ export interface INavigationControllerCallbacks {
onRouteCalculated: (event: IRouteCalculatedEvent) => void;
onRequestUpdate: () => void;
getMap: () => maplibregl.Map | null;
/** Optional callback to fetch traffic-aware route */
getTrafficRoute?: (
start: [number, number],
end: [number, number],
mode: TNavigationMode
) => Promise<ITrafficAwareRoute | null>;
}
/**
@@ -71,6 +79,7 @@ export class NavigationController {
startAddress: '',
endAddress: '',
route: null,
trafficRoute: null,
isLoading: false,
error: null,
};
@@ -154,20 +163,30 @@ export class NavigationController {
...this.navigationState,
isLoading: true,
error: null,
trafficRoute: null,
};
this.callbacks.onRequestUpdate();
try {
const route = await this.fetchRoute(startPoint, endPoint, this.navigationMode);
// Fetch both regular route and traffic-aware route in parallel
const [route, trafficRoute] = await Promise.all([
this.fetchRoute(startPoint, endPoint, this.navigationMode),
this.callbacks.getTrafficRoute
? this.callbacks.getTrafficRoute(startPoint, endPoint, this.navigationMode)
: Promise.resolve(null),
]);
if (route) {
this.navigationState = {
...this.navigationState,
route,
trafficRoute,
isLoading: false,
};
this.renderRouteOnMap(route);
// Use traffic route geometry if available, otherwise use regular route
const routeToRender = trafficRoute || route;
this.renderRouteOnMap(routeToRender);
// Dispatch route-calculated event
this.callbacks.onRouteCalculated({
@@ -178,7 +197,7 @@ export class NavigationController {
});
// Fit map to route bounds
this.fitToRoute(route);
this.fitToRoute(routeToRender);
}
} catch (error) {
this.navigationState = {
@@ -244,6 +263,7 @@ export class NavigationController {
startAddress: '',
endAddress: '',
route: null,
trafficRoute: null,
isLoading: false,
error: null,
};
@@ -679,6 +699,19 @@ export class NavigationController {
return icons[key] || icons[type] || '➡';
}
/**
* Get congestion label for display
*/
public getCongestionLabel(level: 'low' | 'moderate' | 'heavy' | 'severe'): string {
const labels = {
low: 'Light traffic',
moderate: 'Moderate traffic',
heavy: 'Heavy traffic',
severe: 'Severe congestion',
};
return labels[level];
}
/**
* Format step instruction for display
*/
@@ -754,13 +787,14 @@ export class NavigationController {
/**
* Render the navigation panel
* @param extraClass - Optional CSS class to add to the panel for positioning
*/
public render(): TemplateResult {
public render(extraClass?: string): TemplateResult {
const { route, isLoading, error, startPoint, endPoint } = this.navigationState;
const canCalculate = startPoint && endPoint && !isLoading;
return html`
<div class="navigation-panel">
<div class="navigation-panel ${extraClass || ''}">
<div class="nav-header">
<div class="nav-header-icon">${renderIcon('navigation')}</div>
<span class="nav-header-title">Navigation</span>
@@ -833,10 +867,22 @@ export class NavigationController {
</div>
<div class="nav-summary-item">
${renderIcon('clock')}
<span>${this.formatDuration(route.duration)}</span>
<span>${this.formatDuration(this.navigationState.trafficRoute?.duration ?? route.duration)}</span>
</div>
</div>
${this.navigationState.trafficRoute ? html`
<div class="nav-traffic-info ${this.navigationState.trafficRoute.congestionLevel}">
<span class="nav-traffic-indicator ${this.navigationState.trafficRoute.congestionLevel}"></span>
<span class="nav-traffic-text">${this.getCongestionLabel(this.navigationState.trafficRoute.congestionLevel)}</span>
${this.navigationState.trafficRoute.duration > this.navigationState.trafficRoute.durationWithoutTraffic ? html`
<span class="nav-traffic-delay">
+${this.formatDuration(this.navigationState.trafficRoute.duration - this.navigationState.trafficRoute.durationWithoutTraffic)} due to traffic
</span>
` : ''}
</div>
` : ''}
<div class="nav-steps">
${this.renderTurnByTurn(route)}
</div>