code icon Code

Get Directions

Get driving directions between two locations using OSRM (Open Source Routing Machine)

Source Code

const [origin, destination, format = "summary"] = process.argv.slice(2);

if (!origin || !destination) {
  console.error("Error: both origin and destination are required");
  process.exit(1);
}

const USER_AGENT = "SaunaGeoOperations/1.0";

// Check if string looks like coordinates
function isCoordinates(str) {
  const coordPattern = /^-?\d+\.?\d*,\s*-?\d+\.?\d*$/;
  return coordPattern.test(str.trim());
}

// Parse coordinate string
function parseCoordinates(str) {
  const [lat, lon] = str.split(",").map((s) => parseFloat(s.trim()));
  return { lat, lon };
}

// Geocode address to coordinates
async function geocode(address) {
  const params = new URLSearchParams({
    q: address,
    format: "json",
    limit: "1",
  });

  const response = await fetch(
    `https://nominatim.openstreetmap.org/search?${params}`,
    { headers: { "User-Agent": USER_AGENT } }
  );

  if (!response.ok) {
    throw new Error(`Geocoding failed: ${response.status}`);
  }

  const results = await response.json();
  if (!results.length) {
    throw new Error(`Location not found: "${address}"`);
  }

  return {
    lat: parseFloat(results[0].lat),
    lon: parseFloat(results[0].lon),
    displayName: results[0].display_name,
  };
}

// Get location coordinates (geocode if needed)
async function resolveLocation(location) {
  if (isCoordinates(location)) {
    const coords = parseCoordinates(location);
    return { ...coords, displayName: `${coords.lat}, ${coords.lon}` };
  }
  return await geocode(location);
}

// Format duration in human-readable form
function formatDuration(seconds) {
  const hours = Math.floor(seconds / 3600);
  const minutes = Math.floor((seconds % 3600) / 60);

  if (hours > 0) {
    return `${hours}h ${minutes}m`;
  }
  return `${minutes} min`;
}

// Format distance
function formatDistance(meters) {
  if (meters >= 1000) {
    return `${(meters / 1000).toFixed(1)} km`;
  }
  return `${Math.round(meters)} m`;
}

// Get OSRM route
async function getRoute(originCoords, destCoords) {
  // OSRM expects lon,lat order (not lat,lon)
  const coords = `${originCoords.lon},${originCoords.lat};${destCoords.lon},${destCoords.lat}`;

  const url = `https://router.project-osrm.org/route/v1/driving/${coords}?overview=full&steps=true&geometries=geojson`;

  const response = await fetch(url, {
    headers: { "User-Agent": USER_AGENT },
  });

  if (!response.ok) {
    throw new Error(`Routing failed: ${response.status}`);
  }

  const data = await response.json();

  if (data.code !== "Ok") {
    throw new Error(`Routing failed: ${data.code} - ${data.message || ""}`);
  }

  return data.routes[0];
}

// Parse OSRM step into readable instruction
function parseStep(step) {
  const maneuver = step.maneuver;
  let instruction = "";

  switch (maneuver.type) {
    case "depart":
      instruction = `Start on ${step.name || "the road"}`;
      break;
    case "arrive":
      instruction = "Arrive at destination";
      break;
    case "turn":
      instruction = `Turn ${maneuver.modifier || ""} onto ${step.name || "the road"}`;
      break;
    case "continue":
      instruction = `Continue on ${step.name || "the road"}`;
      break;
    case "merge":
      instruction = `Merge onto ${step.name || "the road"}`;
      break;
    case "fork":
      instruction = `Take the ${maneuver.modifier || ""} fork onto ${step.name || "the road"}`;
      break;
    case "roundabout":
      instruction = `Take exit ${maneuver.exit || ""} from roundabout onto ${step.name || "the road"}`;
      break;
    default:
      instruction = `${maneuver.type} ${maneuver.modifier || ""} - ${step.name || "continue"}`;
  }

  return {
    instruction: instruction.trim(),
    distance: formatDistance(step.distance),
    duration: formatDuration(step.duration),
  };
}

async function main() {
  console.log(`Getting directions from "${origin}" to "${destination}"`);

  // Resolve both locations
  console.log("Resolving origin...");
  const originCoords = await resolveLocation(origin);
  console.log(`Origin: ${originCoords.displayName}`);

  // Add small delay to respect Nominatim rate limit
  if (!isCoordinates(origin)) {
    await new Promise((r) => setTimeout(r, 1100));
  }

  console.log("Resolving destination...");
  const destCoords = await resolveLocation(destination);
  console.log(`Destination: ${destCoords.displayName}`);

  // Get route from OSRM
  console.log("Calculating route...");
  const route = await getRoute(originCoords, destCoords);

  const result = {
    success: true,
    origin: {
      query: origin,
      displayName: originCoords.displayName,
      coordinates: { lat: originCoords.lat, lon: originCoords.lon },
    },
    destination: {
      query: destination,
      displayName: destCoords.displayName,
      coordinates: { lat: destCoords.lat, lon: destCoords.lon },
    },
    route: {
      distance: formatDistance(route.distance),
      distanceMeters: route.distance,
      duration: formatDuration(route.duration),
      durationSeconds: route.duration,
    },
  };

  console.log(`\nRoute: ${result.route.distance}, ${result.route.duration}`);

  if (format === "detailed") {
    // Extract steps from all legs
    const steps = [];
    for (const leg of route.legs) {
      for (const step of leg.steps) {
        steps.push(parseStep(step));
      }
    }
    result.steps = steps;

    console.log("\nDirections:");
    steps.forEach((step, i) => {
      console.log(`${i + 1}. ${step.instruction} (${step.distance})`);
    });
  }

  console.log(JSON.stringify(result));
}

main().catch((err) => {
  console.error("Routing failed:", err.message);
  process.exit(1);
});