767 lines
22 KiB
TypeScript
767 lines
22 KiB
TypeScript
import type { TNavigationMode, IOSRMLeg } from './geo-map.navigation.js';
|
|
|
|
// ─── Traffic Data Types ──────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* A segment of road with traffic flow data
|
|
*/
|
|
export interface ITrafficFlowSegment {
|
|
/** GeoJSON LineString geometry for the segment */
|
|
geometry: GeoJSON.LineString;
|
|
/** Congestion level from 0 (free flow) to 1 (blocked) */
|
|
congestion: number;
|
|
/** Current speed in km/h */
|
|
speed: number;
|
|
/** Free-flow (normal) speed in km/h */
|
|
freeFlowSpeed: number;
|
|
/** Confidence/quality of the data (0-1) */
|
|
confidence: number;
|
|
}
|
|
|
|
/**
|
|
* Traffic flow data for a geographic area
|
|
*/
|
|
export interface ITrafficFlowData {
|
|
/** Array of road segments with traffic data */
|
|
segments: ITrafficFlowSegment[];
|
|
/** Timestamp when the data was fetched */
|
|
timestamp: Date;
|
|
/** Bounding box [west, south, east, north] */
|
|
bounds: [number, number, number, number];
|
|
}
|
|
|
|
/**
|
|
* A route with traffic-aware duration estimates
|
|
*/
|
|
export interface ITrafficAwareRoute {
|
|
/** Route geometry */
|
|
geometry: GeoJSON.LineString;
|
|
/** Total distance in meters */
|
|
distance: number;
|
|
/** Duration with current traffic in seconds */
|
|
duration: number;
|
|
/** Duration without traffic (free-flow) in seconds */
|
|
durationWithoutTraffic: number;
|
|
/** Overall congestion level for the route */
|
|
congestionLevel: 'low' | 'moderate' | 'heavy' | 'severe';
|
|
/** Route legs with turn-by-turn directions */
|
|
legs: IOSRMLeg[];
|
|
}
|
|
|
|
/**
|
|
* Configuration options for traffic providers
|
|
*/
|
|
export interface ITrafficProviderConfig {
|
|
/** API key (for providers that require authentication) */
|
|
apiKey?: string;
|
|
/** Server URL (for self-hosted providers) */
|
|
serverUrl?: string;
|
|
/** Refresh interval in milliseconds */
|
|
refreshInterval?: number;
|
|
/** Whether to include traffic incidents */
|
|
includeIncidents?: boolean;
|
|
/** Additional provider-specific options */
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
// ─── Traffic Provider Interface ──────────────────────────────────────────────
|
|
|
|
/**
|
|
* Interface for traffic data providers
|
|
*/
|
|
export interface ITrafficProvider {
|
|
/** Provider name */
|
|
readonly name: string;
|
|
|
|
/** Whether the provider is configured and ready to use */
|
|
readonly isConfigured: boolean;
|
|
|
|
/**
|
|
* Fetch traffic flow data for a geographic area
|
|
* @param bounds - [west, south, east, north] bounding box
|
|
* @returns Traffic flow data or null if unavailable
|
|
*/
|
|
fetchTrafficFlow(bounds: [number, number, number, number]): Promise<ITrafficFlowData | null>;
|
|
|
|
/**
|
|
* Fetch a route with traffic-aware duration estimates (optional)
|
|
* @param start - Start coordinates [lng, lat]
|
|
* @param end - End coordinates [lng, lat]
|
|
* @param mode - Navigation mode
|
|
* @returns Traffic-aware route or null if not supported
|
|
*/
|
|
fetchRouteWithTraffic?(
|
|
start: [number, number],
|
|
end: [number, number],
|
|
mode: TNavigationMode
|
|
): Promise<ITrafficAwareRoute | null>;
|
|
|
|
/**
|
|
* Configure the provider with options
|
|
* @param options - Provider configuration
|
|
*/
|
|
configure(options: ITrafficProviderConfig): void;
|
|
}
|
|
|
|
// ─── HERE Traffic Provider ───────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Traffic provider using HERE Traffic API v7 (freemium)
|
|
*
|
|
* Free tier includes:
|
|
* - 250,000 transactions/month
|
|
* - Traffic flow and incidents
|
|
* - No credit card required
|
|
*
|
|
* @see https://developer.here.com/documentation/traffic-api/dev_guide/index.html
|
|
*/
|
|
export class HereTrafficProvider implements ITrafficProvider {
|
|
public readonly name = 'HERE Traffic API';
|
|
private apiKey: string = '';
|
|
private includeIncidents: boolean = true;
|
|
|
|
public get isConfigured(): boolean {
|
|
return this.apiKey.length > 0;
|
|
}
|
|
|
|
public configure(options: ITrafficProviderConfig): void {
|
|
if (options.apiKey) {
|
|
this.apiKey = options.apiKey;
|
|
}
|
|
if (typeof options.includeIncidents === 'boolean') {
|
|
this.includeIncidents = options.includeIncidents;
|
|
}
|
|
}
|
|
|
|
public async fetchTrafficFlow(
|
|
bounds: [number, number, number, number]
|
|
): Promise<ITrafficFlowData | null> {
|
|
if (!this.isConfigured) {
|
|
console.warn('[HereTrafficProvider] No API key configured');
|
|
return null;
|
|
}
|
|
|
|
const [west, south, east, north] = bounds;
|
|
|
|
// HERE Traffic Flow API endpoint
|
|
// Format: bbox=west,south,east,north
|
|
const bbox = `${west},${south},${east},${north}`;
|
|
const url = `https://data.traffic.hereapi.com/v7/flow?in=bbox:${bbox}&apiKey=${this.apiKey}`;
|
|
|
|
try {
|
|
const response = await fetch(url);
|
|
|
|
if (!response.ok) {
|
|
if (response.status === 401 || response.status === 403) {
|
|
console.error('[HereTrafficProvider] Invalid or expired API key');
|
|
} else if (response.status === 429) {
|
|
console.warn('[HereTrafficProvider] Rate limit exceeded');
|
|
} else {
|
|
console.error(`[HereTrafficProvider] API error: ${response.status}`);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
const data = await response.json();
|
|
return this.parseHereTrafficFlow(data, bounds);
|
|
} catch (error) {
|
|
console.error('[HereTrafficProvider] Fetch error:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse HERE Traffic API response into our standard format
|
|
*/
|
|
private parseHereTrafficFlow(
|
|
data: IHereTrafficFlowResponse,
|
|
bounds: [number, number, number, number]
|
|
): ITrafficFlowData {
|
|
const segments: ITrafficFlowSegment[] = [];
|
|
|
|
if (data.results) {
|
|
for (const result of data.results) {
|
|
if (!result.location?.shape?.links) continue;
|
|
|
|
for (const link of result.location.shape.links) {
|
|
// Parse the polyline points
|
|
const coordinates = this.parseHerePolyline(link.points);
|
|
if (coordinates.length < 2) continue;
|
|
|
|
// Get traffic data from currentFlow
|
|
const flow = result.currentFlow;
|
|
const freeFlow = flow?.freeFlow || flow?.speed || 50;
|
|
const currentSpeed = flow?.speed || freeFlow;
|
|
const jamFactor = flow?.jamFactor || 0; // 0-10 scale
|
|
|
|
segments.push({
|
|
geometry: {
|
|
type: 'LineString',
|
|
coordinates,
|
|
},
|
|
congestion: Math.min(jamFactor / 10, 1), // Normalize to 0-1
|
|
speed: currentSpeed,
|
|
freeFlowSpeed: freeFlow,
|
|
confidence: flow?.confidence || 0.5,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
segments,
|
|
timestamp: new Date(),
|
|
bounds,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Parse HERE polyline format to coordinates array
|
|
* HERE uses lat,lng format, we need lng,lat
|
|
*/
|
|
private parseHerePolyline(points: IHerePoint[]): [number, number][] {
|
|
return points.map(point => [point.lng, point.lat] as [number, number]);
|
|
}
|
|
|
|
public async fetchRouteWithTraffic(
|
|
start: [number, number],
|
|
end: [number, number],
|
|
mode: TNavigationMode
|
|
): Promise<ITrafficAwareRoute | null> {
|
|
if (!this.isConfigured) {
|
|
console.warn('[HereTrafficProvider] No API key configured');
|
|
return null;
|
|
}
|
|
|
|
// Map mode to HERE transport mode
|
|
const transportMode = mode === 'cycling' ? 'bicycle'
|
|
: mode === 'walking' ? 'pedestrian'
|
|
: 'car';
|
|
|
|
// HERE Routing API v8 with traffic
|
|
const url = new URL('https://router.hereapi.com/v8/routes');
|
|
url.searchParams.set('apiKey', this.apiKey);
|
|
url.searchParams.set('origin', `${start[1]},${start[0]}`); // HERE uses lat,lng
|
|
url.searchParams.set('destination', `${end[1]},${end[0]}`);
|
|
url.searchParams.set('transportMode', transportMode);
|
|
url.searchParams.set('return', 'polyline,summary,turnByTurnActions');
|
|
|
|
// Include traffic for driving mode
|
|
if (mode === 'driving') {
|
|
url.searchParams.set('departureTime', 'now'); // Enables live traffic
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(url.toString());
|
|
|
|
if (!response.ok) {
|
|
console.error(`[HereTrafficProvider] Routing error: ${response.status}`);
|
|
return null;
|
|
}
|
|
|
|
const data = await response.json() as IHereRoutingResponse;
|
|
return this.parseHereRoute(data);
|
|
} catch (error) {
|
|
console.error('[HereTrafficProvider] Routing fetch error:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse HERE Routing API response
|
|
*/
|
|
private parseHereRoute(data: IHereRoutingResponse): ITrafficAwareRoute | null {
|
|
if (!data.routes || data.routes.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const route = data.routes[0];
|
|
const section = route.sections?.[0];
|
|
if (!section) return null;
|
|
|
|
// Decode the polyline
|
|
const coordinates = this.decodeFlexiblePolyline(section.polyline);
|
|
|
|
// Calculate congestion level based on delay
|
|
const baseDuration = section.summary?.baseDuration || section.summary?.duration || 0;
|
|
const duration = section.summary?.duration || baseDuration;
|
|
const delayRatio = baseDuration > 0 ? (duration - baseDuration) / baseDuration : 0;
|
|
|
|
let congestionLevel: 'low' | 'moderate' | 'heavy' | 'severe';
|
|
if (delayRatio < 0.1) congestionLevel = 'low';
|
|
else if (delayRatio < 0.3) congestionLevel = 'moderate';
|
|
else if (delayRatio < 0.5) congestionLevel = 'heavy';
|
|
else congestionLevel = 'severe';
|
|
|
|
// Convert HERE actions to OSRM-style legs/steps
|
|
const legs = this.convertToOsrmLegs(section);
|
|
|
|
return {
|
|
geometry: {
|
|
type: 'LineString',
|
|
coordinates,
|
|
},
|
|
distance: section.summary?.length || 0,
|
|
duration,
|
|
durationWithoutTraffic: baseDuration,
|
|
congestionLevel,
|
|
legs,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Decode HERE's flexible polyline format
|
|
* @see https://github.com/heremaps/flexible-polyline
|
|
*/
|
|
private decodeFlexiblePolyline(encoded: string): [number, number][] {
|
|
// Simplified decoder - for full support, use @here/flexpolyline package
|
|
const coordinates: [number, number][] = [];
|
|
|
|
let index = 0;
|
|
let lat = 0;
|
|
let lng = 0;
|
|
|
|
// Skip header byte (version and precision info)
|
|
const header = this.decodeUnsignedValue(encoded, index);
|
|
index = header.nextIndex;
|
|
const precision = Math.pow(10, header.value & 15);
|
|
|
|
while (index < encoded.length) {
|
|
const latResult = this.decodeSignedValue(encoded, index);
|
|
index = latResult.nextIndex;
|
|
lat += latResult.value;
|
|
|
|
const lngResult = this.decodeSignedValue(encoded, index);
|
|
index = lngResult.nextIndex;
|
|
lng += lngResult.value;
|
|
|
|
coordinates.push([lng / precision, lat / precision]);
|
|
}
|
|
|
|
return coordinates;
|
|
}
|
|
|
|
private decodeUnsignedValue(encoded: string, startIndex: number): { value: number; nextIndex: number } {
|
|
let result = 0;
|
|
let shift = 0;
|
|
let index = startIndex;
|
|
|
|
while (index < encoded.length) {
|
|
const char = encoded.charCodeAt(index) - 45;
|
|
index++;
|
|
result |= (char & 31) << shift;
|
|
if (char < 32) break;
|
|
shift += 5;
|
|
}
|
|
|
|
return { value: result, nextIndex: index };
|
|
}
|
|
|
|
private decodeSignedValue(encoded: string, startIndex: number): { value: number; nextIndex: number } {
|
|
const { value, nextIndex } = this.decodeUnsignedValue(encoded, startIndex);
|
|
return {
|
|
value: (value & 1) ? ~(value >> 1) : (value >> 1),
|
|
nextIndex,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Convert HERE routing actions to OSRM-style legs
|
|
*/
|
|
private convertToOsrmLegs(section: IHereRouteSection): IOSRMLeg[] {
|
|
const steps = (section.turnByTurnActions || []).map(action => ({
|
|
geometry: { type: 'LineString' as const, coordinates: [] as [number, number][] },
|
|
maneuver: {
|
|
type: this.mapHereActionToOsrm(action.action),
|
|
modifier: action.direction?.toLowerCase(),
|
|
location: [action.offset || 0, 0] as [number, number], // Simplified
|
|
},
|
|
name: action.currentRoad?.name?.[0]?.value || action.nextRoad?.name?.[0]?.value || '',
|
|
distance: action.length || 0,
|
|
duration: action.duration || 0,
|
|
driving_side: 'right',
|
|
}));
|
|
|
|
return [{
|
|
steps,
|
|
distance: section.summary?.length || 0,
|
|
duration: section.summary?.duration || 0,
|
|
}];
|
|
}
|
|
|
|
/**
|
|
* Map HERE action types to OSRM maneuver types
|
|
*/
|
|
private mapHereActionToOsrm(action: string): string {
|
|
const mapping: Record<string, string> = {
|
|
'depart': 'depart',
|
|
'arrive': 'arrive',
|
|
'turn': 'turn',
|
|
'continue': 'continue',
|
|
'roundaboutEnter': 'roundabout',
|
|
'roundaboutExit': 'roundabout',
|
|
'merge': 'merge',
|
|
'fork': 'fork',
|
|
'uturn': 'turn',
|
|
};
|
|
return mapping[action] || action;
|
|
}
|
|
}
|
|
|
|
// ─── Valhalla Traffic Provider ───────────────────────────────────────────────
|
|
|
|
/**
|
|
* Traffic provider for self-hosted Valhalla servers
|
|
*
|
|
* Requires:
|
|
* - A running Valhalla server with traffic data enabled
|
|
* - Traffic data in Valhalla's expected format
|
|
*
|
|
* @see https://valhalla.github.io/valhalla/
|
|
*/
|
|
export class ValhallaTrafficProvider implements ITrafficProvider {
|
|
public readonly name = 'Valhalla (Self-Hosted)';
|
|
private serverUrl: string = '';
|
|
private trafficDataUrl: string = '';
|
|
|
|
public get isConfigured(): boolean {
|
|
return this.serverUrl.length > 0;
|
|
}
|
|
|
|
public configure(options: ITrafficProviderConfig): void {
|
|
if (options.serverUrl) {
|
|
this.serverUrl = options.serverUrl.replace(/\/$/, ''); // Remove trailing slash
|
|
}
|
|
if (options.trafficDataUrl && typeof options.trafficDataUrl === 'string') {
|
|
this.trafficDataUrl = options.trafficDataUrl;
|
|
}
|
|
}
|
|
|
|
public async fetchTrafficFlow(
|
|
bounds: [number, number, number, number]
|
|
): Promise<ITrafficFlowData | null> {
|
|
if (!this.isConfigured) {
|
|
console.warn('[ValhallaTrafficProvider] No server URL configured');
|
|
return null;
|
|
}
|
|
|
|
// Valhalla doesn't have a direct traffic flow API like HERE
|
|
// This would require custom traffic data endpoint setup
|
|
if (!this.trafficDataUrl) {
|
|
console.info('[ValhallaTrafficProvider] Traffic flow visualization requires trafficDataUrl');
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
const [west, south, east, north] = bounds;
|
|
const url = `${this.trafficDataUrl}?bbox=${west},${south},${east},${north}`;
|
|
const response = await fetch(url);
|
|
|
|
if (!response.ok) {
|
|
console.error(`[ValhallaTrafficProvider] Traffic data error: ${response.status}`);
|
|
return null;
|
|
}
|
|
|
|
const data = await response.json();
|
|
return this.parseValhallaTrafficData(data, bounds);
|
|
} catch (error) {
|
|
console.error('[ValhallaTrafficProvider] Fetch error:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse custom traffic data format (user-defined schema)
|
|
*/
|
|
private parseValhallaTrafficData(
|
|
data: IValhallaTrafficData,
|
|
bounds: [number, number, number, number]
|
|
): ITrafficFlowData {
|
|
const segments: ITrafficFlowSegment[] = [];
|
|
|
|
if (data.segments) {
|
|
for (const seg of data.segments) {
|
|
segments.push({
|
|
geometry: seg.geometry,
|
|
congestion: seg.congestion || 0,
|
|
speed: seg.speed || 0,
|
|
freeFlowSpeed: seg.freeFlowSpeed || seg.speed || 50,
|
|
confidence: seg.confidence || 0.5,
|
|
});
|
|
}
|
|
}
|
|
|
|
return {
|
|
segments,
|
|
timestamp: new Date(),
|
|
bounds,
|
|
};
|
|
}
|
|
|
|
public async fetchRouteWithTraffic(
|
|
start: [number, number],
|
|
end: [number, number],
|
|
mode: TNavigationMode
|
|
): Promise<ITrafficAwareRoute | null> {
|
|
if (!this.isConfigured) {
|
|
console.warn('[ValhallaTrafficProvider] No server URL configured');
|
|
return null;
|
|
}
|
|
|
|
// Map mode to Valhalla costing model
|
|
const costing = mode === 'cycling' ? 'bicycle'
|
|
: mode === 'walking' ? 'pedestrian'
|
|
: 'auto';
|
|
|
|
const requestBody = {
|
|
locations: [
|
|
{ lat: start[1], lon: start[0] },
|
|
{ lat: end[1], lon: end[0] },
|
|
],
|
|
costing,
|
|
costing_options: {
|
|
[costing]: {
|
|
use_traffic: true, // Enable traffic consideration
|
|
},
|
|
},
|
|
directions_options: {
|
|
units: 'kilometers',
|
|
},
|
|
};
|
|
|
|
try {
|
|
const response = await fetch(`${this.serverUrl}/route`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(requestBody),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
console.error(`[ValhallaTrafficProvider] Routing error: ${response.status}`);
|
|
return null;
|
|
}
|
|
|
|
const data = await response.json() as IValhallaRouteResponse;
|
|
return this.parseValhallaRoute(data);
|
|
} catch (error) {
|
|
console.error('[ValhallaTrafficProvider] Routing fetch error:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse Valhalla routing response
|
|
*/
|
|
private parseValhallaRoute(data: IValhallaRouteResponse): ITrafficAwareRoute | null {
|
|
if (!data.trip?.legs?.[0]) {
|
|
return null;
|
|
}
|
|
|
|
const leg = data.trip.legs[0];
|
|
const summary = data.trip.summary;
|
|
|
|
// Decode Valhalla's polyline6 format
|
|
const coordinates = this.decodePolyline6(leg.shape);
|
|
|
|
// Calculate congestion level
|
|
const delayRatio = summary.time > 0
|
|
? (summary.time - (summary.time * 0.9)) / summary.time // Estimate without traffic
|
|
: 0;
|
|
|
|
let congestionLevel: 'low' | 'moderate' | 'heavy' | 'severe';
|
|
if (delayRatio < 0.1) congestionLevel = 'low';
|
|
else if (delayRatio < 0.3) congestionLevel = 'moderate';
|
|
else if (delayRatio < 0.5) congestionLevel = 'heavy';
|
|
else congestionLevel = 'severe';
|
|
|
|
// Convert Valhalla maneuvers to OSRM format
|
|
const steps = (leg.maneuvers || []).map(maneuver => ({
|
|
geometry: { type: 'LineString' as const, coordinates: [] as [number, number][] },
|
|
maneuver: {
|
|
type: this.mapValhallaManeuverToOsrm(maneuver.type),
|
|
modifier: maneuver.modifier,
|
|
location: [maneuver.begin_shape_index, 0] as [number, number],
|
|
},
|
|
name: maneuver.street_names?.[0] || '',
|
|
distance: maneuver.length * 1000, // Convert km to meters
|
|
duration: maneuver.time,
|
|
driving_side: 'right',
|
|
}));
|
|
|
|
return {
|
|
geometry: {
|
|
type: 'LineString',
|
|
coordinates,
|
|
},
|
|
distance: summary.length * 1000, // Convert km to meters
|
|
duration: summary.time,
|
|
durationWithoutTraffic: summary.time * 0.9, // Estimate
|
|
congestionLevel,
|
|
legs: [{
|
|
steps,
|
|
distance: summary.length * 1000,
|
|
duration: summary.time,
|
|
}],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Decode polyline6 format (Valhalla uses 6 decimal precision)
|
|
*/
|
|
private decodePolyline6(encoded: string): [number, number][] {
|
|
const coordinates: [number, number][] = [];
|
|
let index = 0;
|
|
let lat = 0;
|
|
let lng = 0;
|
|
|
|
while (index < encoded.length) {
|
|
// Decode latitude
|
|
let shift = 0;
|
|
let result = 0;
|
|
let byte: number;
|
|
|
|
do {
|
|
byte = encoded.charCodeAt(index++) - 63;
|
|
result |= (byte & 0x1f) << shift;
|
|
shift += 5;
|
|
} while (byte >= 0x20);
|
|
|
|
lat += (result & 1) ? ~(result >> 1) : (result >> 1);
|
|
|
|
// Decode longitude
|
|
shift = 0;
|
|
result = 0;
|
|
|
|
do {
|
|
byte = encoded.charCodeAt(index++) - 63;
|
|
result |= (byte & 0x1f) << shift;
|
|
shift += 5;
|
|
} while (byte >= 0x20);
|
|
|
|
lng += (result & 1) ? ~(result >> 1) : (result >> 1);
|
|
|
|
coordinates.push([lng / 1e6, lat / 1e6]);
|
|
}
|
|
|
|
return coordinates;
|
|
}
|
|
|
|
/**
|
|
* Map Valhalla maneuver types to OSRM types
|
|
*/
|
|
private mapValhallaManeuverToOsrm(type: number): string {
|
|
const mapping: Record<number, string> = {
|
|
0: 'none',
|
|
1: 'depart',
|
|
2: 'depart',
|
|
3: 'arrive',
|
|
4: 'arrive',
|
|
5: 'arrive',
|
|
6: 'continue',
|
|
7: 'continue',
|
|
8: 'turn',
|
|
9: 'turn',
|
|
10: 'turn',
|
|
11: 'turn',
|
|
12: 'turn',
|
|
13: 'turn',
|
|
14: 'turn',
|
|
15: 'turn',
|
|
16: 'turn',
|
|
17: 'roundabout',
|
|
18: 'roundabout',
|
|
19: 'roundabout',
|
|
20: 'roundabout',
|
|
21: 'fork',
|
|
22: 'fork',
|
|
23: 'merge',
|
|
24: 'merge',
|
|
25: 'merge',
|
|
26: 'notification',
|
|
27: 'notification',
|
|
};
|
|
return mapping[type] || 'continue';
|
|
}
|
|
}
|
|
|
|
// ─── HERE API Response Types ─────────────────────────────────────────────────
|
|
|
|
interface IHerePoint {
|
|
lat: number;
|
|
lng: number;
|
|
}
|
|
|
|
interface IHereTrafficFlowResponse {
|
|
results?: Array<{
|
|
location?: {
|
|
shape?: {
|
|
links?: Array<{
|
|
points: IHerePoint[];
|
|
}>;
|
|
};
|
|
};
|
|
currentFlow?: {
|
|
speed?: number;
|
|
freeFlow?: number;
|
|
jamFactor?: number;
|
|
confidence?: number;
|
|
};
|
|
}>;
|
|
}
|
|
|
|
interface IHereRoutingResponse {
|
|
routes?: Array<{
|
|
sections?: IHereRouteSection[];
|
|
}>;
|
|
}
|
|
|
|
interface IHereRouteSection {
|
|
polyline: string;
|
|
summary?: {
|
|
duration: number;
|
|
baseDuration?: number;
|
|
length: number;
|
|
};
|
|
turnByTurnActions?: Array<{
|
|
action: string;
|
|
direction?: string;
|
|
offset?: number;
|
|
length?: number;
|
|
duration?: number;
|
|
currentRoad?: { name?: Array<{ value: string }> };
|
|
nextRoad?: { name?: Array<{ value: string }> };
|
|
}>;
|
|
}
|
|
|
|
// ─── Valhalla API Response Types ─────────────────────────────────────────────
|
|
|
|
interface IValhallaTrafficData {
|
|
segments?: Array<{
|
|
geometry: GeoJSON.LineString;
|
|
congestion?: number;
|
|
speed?: number;
|
|
freeFlowSpeed?: number;
|
|
confidence?: number;
|
|
}>;
|
|
}
|
|
|
|
interface IValhallaRouteResponse {
|
|
trip?: {
|
|
summary: {
|
|
time: number;
|
|
length: number;
|
|
};
|
|
legs?: Array<{
|
|
shape: string;
|
|
maneuvers?: Array<{
|
|
type: number;
|
|
modifier?: string;
|
|
begin_shape_index: number;
|
|
street_names?: string[];
|
|
length: number;
|
|
time: number;
|
|
}>;
|
|
}>;
|
|
};
|
|
}
|