feat(geo-map): add live traffic visualization and traffic-aware routing with pluggable providers and UI integration
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user