195 lines
6.3 KiB
TypeScript
195 lines
6.3 KiB
TypeScript
import axios from "axios";
|
|
|
|
const TOMTOM_API_KEY = "MERY8I7LMeYVSLKO5WuV73W9rKJpBLoB";
|
|
|
|
export interface LatLng {
|
|
latitude: number;
|
|
longitude: number;
|
|
}
|
|
|
|
export interface RouteInfo {
|
|
distance: string;
|
|
duration: string;
|
|
distanceMeters: number;
|
|
durationSeconds: number;
|
|
coordinates: LatLng[];
|
|
}
|
|
|
|
export interface NavigationInstruction {
|
|
instruction: string;
|
|
distance: string;
|
|
maneuver: string;
|
|
streetName?: string;
|
|
isActive: boolean;
|
|
}
|
|
|
|
export const maneuverTranslations: Record<string, string> = {
|
|
TURN_LEFT: "Tournez à gauche",
|
|
TURN_RIGHT: "Tournez à droite",
|
|
TURN_SLIGHT_LEFT: "Tournez légèrement à gauche",
|
|
TURN_SLIGHT_RIGHT: "Tournez légèrement à droite",
|
|
TURN_SHARP_LEFT: "Tournez fortement à gauche",
|
|
TURN_SHARP_RIGHT: "Tournez fortement à droite",
|
|
KEEP_LEFT: "Restez à gauche",
|
|
KEEP_RIGHT: "Restez à droite",
|
|
STRAIGHT: "Continuez tout droit",
|
|
ENTER_ROUNDABOUT: "Entrez dans le rond-point",
|
|
EXIT_ROUNDABOUT: "Sortez du rond-point",
|
|
MOTORWAY_ENTER: "Entrez sur l'autoroute",
|
|
MOTORWAY_EXIT: "Sortez de l'autoroute",
|
|
ARRIVE: "Vous êtes arrivé",
|
|
ARRIVE_LEFT: "Destination à gauche",
|
|
ARRIVE_RIGHT: "Destination à droite",
|
|
DEPART: "Départ",
|
|
U_TURN: "Faites demi-tour",
|
|
FOLLOW: "Suivez",
|
|
WAYPOINT_REACHED: "Point de passage atteint",
|
|
};
|
|
|
|
export const maneuverIcons: Record<string, string> = {
|
|
TURN_LEFT: "arrow-back",
|
|
TURN_RIGHT: "arrow-forward",
|
|
TURN_SLIGHT_LEFT: "arrow-up",
|
|
TURN_SLIGHT_RIGHT: "arrow-up",
|
|
TURN_SHARP_LEFT: "return-down-back",
|
|
TURN_SHARP_RIGHT: "return-down-forward",
|
|
KEEP_LEFT: "arrow-back",
|
|
KEEP_RIGHT: "arrow-forward",
|
|
STRAIGHT: "arrow-up",
|
|
ENTER_ROUNDABOUT: "sync",
|
|
EXIT_ROUNDABOUT: "arrow-forward",
|
|
MOTORWAY_ENTER: "speedometer",
|
|
MOTORWAY_EXIT: "exit-outline",
|
|
ARRIVE: "flag",
|
|
ARRIVE_LEFT: "flag",
|
|
ARRIVE_RIGHT: "flag",
|
|
DEPART: "car",
|
|
U_TURN: "return-up-back",
|
|
FOLLOW: "arrow-up",
|
|
WAYPOINT_REACHED: "location",
|
|
DEFAULT: "arrow-up",
|
|
};
|
|
|
|
// ---- Helpers ----
|
|
function formatDistance(meters: number): string {
|
|
if (meters < 1000) return `${Math.round(meters)} m`;
|
|
return `${(meters / 1000).toFixed(1)} km`;
|
|
}
|
|
|
|
export async function geocodeAddress(address: string): Promise<LatLng | null> {
|
|
try {
|
|
const url = `https://api.tomtom.com/search/2/geocode/${encodeURIComponent(address)}.json?key=${TOMTOM_API_KEY}&limit=1`;
|
|
const { data } = await axios.get(url);
|
|
if (data.results && data.results.length > 0) {
|
|
const pos = data.results[0].position;
|
|
if (pos?.lat != null && pos?.lon != null) {
|
|
return { latitude: pos.lat, longitude: pos.lon };
|
|
}
|
|
}
|
|
} catch {
|
|
/* silent */
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export async function calculateRoute(
|
|
origin: LatLng,
|
|
destination: LatLng,
|
|
): Promise<{ route: RouteInfo; instructions: NavigationInstruction[] } | null> {
|
|
try {
|
|
const start = `${origin.latitude},${origin.longitude}`;
|
|
const end = `${destination.latitude},${destination.longitude}`;
|
|
const url = `https://api.tomtom.com/routing/1/calculateRoute/${start}:${end}/json?key=${TOMTOM_API_KEY}&instructionsType=text&language=fr-FR&traffic=true&travelMode=car`;
|
|
|
|
const { data } = await axios.get(url);
|
|
|
|
if (!data.routes || data.routes.length === 0) return null;
|
|
|
|
const route = data.routes[0];
|
|
const summary = route.summary;
|
|
|
|
// Extract polyline coordinates
|
|
const coordinates: LatLng[] = [];
|
|
if (route.legs) {
|
|
for (const leg of route.legs) {
|
|
if (leg.points) {
|
|
for (const pt of leg.points) {
|
|
coordinates.push({
|
|
latitude: pt.latitude,
|
|
longitude: pt.longitude,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback: straight line
|
|
if (coordinates.length === 0) {
|
|
coordinates.push(origin, destination);
|
|
}
|
|
|
|
const distanceKm = (summary.lengthInMeters / 1000).toFixed(1);
|
|
const durationMin = Math.round(summary.travelTimeInSeconds / 60);
|
|
|
|
const routeInfo: RouteInfo = {
|
|
distance: `${distanceKm} km`,
|
|
duration: `${durationMin} min`,
|
|
distanceMeters: summary.lengthInMeters,
|
|
durationSeconds: summary.travelTimeInSeconds,
|
|
coordinates,
|
|
};
|
|
|
|
// Extract instructions
|
|
const instructions: NavigationInstruction[] = [];
|
|
if (route.guidance?.instructions) {
|
|
for (let i = 0; i < route.guidance.instructions.length; i++) {
|
|
const inst = route.guidance.instructions[i];
|
|
const maneuver = inst.maneuver || "STRAIGHT";
|
|
const text =
|
|
inst.message ||
|
|
maneuverTranslations[maneuver] ||
|
|
"Continuez";
|
|
instructions.push({
|
|
instruction: text,
|
|
distance: formatDistance(inst.routeOffsetInMeters || 0),
|
|
maneuver,
|
|
streetName: inst.street,
|
|
isActive: i === 0,
|
|
});
|
|
}
|
|
}
|
|
|
|
if (instructions.length === 0) {
|
|
instructions.push({
|
|
instruction: "Dirigez-vous vers votre destination",
|
|
distance: formatDistance(summary.lengthInMeters || 0),
|
|
maneuver: "DEPART",
|
|
isActive: true,
|
|
});
|
|
instructions.push({
|
|
instruction: "Vous êtes arrivé à destination",
|
|
distance: "0 m",
|
|
maneuver: "ARRIVE",
|
|
isActive: false,
|
|
});
|
|
}
|
|
|
|
return { route: routeInfo, instructions };
|
|
} catch {
|
|
/* silent */
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// ---- Bearing calculation ----
|
|
export function calculateBearing(from: LatLng, to: LatLng): number {
|
|
const φ1 = (from.latitude * Math.PI) / 180;
|
|
const φ2 = (to.latitude * Math.PI) / 180;
|
|
const Δλ = ((to.longitude - from.longitude) * Math.PI) / 180;
|
|
const y = Math.sin(Δλ) * Math.cos(φ2);
|
|
const x =
|
|
Math.cos(φ1) * Math.sin(φ2) -
|
|
Math.sin(φ1) * Math.cos(φ2) * Math.cos(Δλ);
|
|
return ((Math.atan2(y, x) * 180) / Math.PI + 360) % 360;
|
|
}
|