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