// Navigation export — turn an optimized groomer route into a deep-link URL that // opens the device's native navigation app (Google Maps / Apple Maps). // // A route is exported as: origin = first stop, destination = last stop, with the // in-between stops carried as ordered intermediate waypoints. Each platform caps // how many intermediate waypoints a deep link may carry, so callers must validate // the route length before handing the URL to the client. /** * Max intermediate waypoints a Google Maps URLs API deep link supports * (`https://www.google.com/maps/dir/?api=1&...&waypoints=...`). Google documents * a ceiling of 9 waypoints between origin and destination. */ export const GOOGLE_MAPS_MAX_WAYPOINTS = 9; /** * Max intermediate waypoints we allow in an Apple Maps `maps://` deep link. Apple's * URL scheme chains destinations with `+to:` but does not publish a hard cap; 15 is * a conservative practical limit that keeps the URL well under length limits. */ export const APPLE_MAPS_MAX_WAYPOINTS = 15; export type NavigationPlatform = "google-maps" | "apple-maps"; /** A single ordered point on the route. `label` is optional, for display only. */ export interface NavigationStop { latitude: number; longitude: number; label?: string | null; } export interface NavigationExportSuccess { platform: NavigationPlatform; url: string; /** Total stops included (origin + waypoints + destination). */ stopCount: number; /** Intermediate waypoints only (excludes origin and destination). */ waypointCount: number; } export interface NavigationExportError { error: string; status: 400; } export type NavigationExportResult = | NavigationExportSuccess | NavigationExportError; function isError(r: NavigationExportResult): r is NavigationExportError { return "error" in r; } /** Intermediate waypoints = every stop that is neither origin nor destination. */ export function intermediateWaypointCount(stopCount: number): number { return Math.max(0, stopCount - 2); } function coord(stop: NavigationStop): string { return `${stop.latitude},${stop.longitude}`; } /** * Builds a Google Maps URLs API driving deep link. On mobile this opens the * native Google Maps app; on desktop it opens maps.google.com. */ export function buildGoogleMapsUrl( stops: NavigationStop[] ): NavigationExportResult { if (stops.length === 0) { return { error: "route has no stops to export", status: 400 }; } const waypointCount = intermediateWaypointCount(stops.length); if (waypointCount > GOOGLE_MAPS_MAX_WAYPOINTS) { return { error: `route has ${waypointCount} intermediate waypoints, exceeding Google Maps' limit of ${GOOGLE_MAPS_MAX_WAYPOINTS}`, status: 400, }; } const origin = stops[0]!; const destination = stops[stops.length - 1]!; const params = new URLSearchParams(); params.set("api", "1"); params.set("travelmode", "driving"); params.set("origin", coord(origin)); params.set("destination", coord(destination)); if (stops.length > 2) { const mids = stops .slice(1, -1) .map(coord) .join("|"); params.set("waypoints", mids); } return { platform: "google-maps", url: `https://www.google.com/maps/dir/?${params.toString()}`, stopCount: stops.length, waypointCount, }; } /** * Builds an Apple Maps `maps://` driving deep link. The first stop is the source * (`saddr`); the remaining stops are chained as destinations with `+to:` (`daddr`). * Built by hand because the `+to:` separators are part of Apple's scheme and must * not be percent-encoded. */ export function buildAppleMapsUrl( stops: NavigationStop[] ): NavigationExportResult { if (stops.length === 0) { return { error: "route has no stops to export", status: 400 }; } const waypointCount = intermediateWaypointCount(stops.length); if (waypointCount > APPLE_MAPS_MAX_WAYPOINTS) { return { error: `route has ${waypointCount} intermediate waypoints, exceeding Apple Maps' limit of ${APPLE_MAPS_MAX_WAYPOINTS}`, status: 400, }; } const params: string[] = ["dirflg=d"]; if (stops.length === 1) { // Single stop: destination only, no source. params.unshift(`daddr=${coord(stops[0]!)}`); } else { const daddr = stops .slice(1) .map(coord) .join("+to:"); params.unshift(`daddr=${daddr}`); params.unshift(`saddr=${coord(stops[0]!)}`); } return { platform: "apple-maps", url: `maps://?${params.join("&")}`, stopCount: stops.length, waypointCount, }; } /** Dispatches to the correct builder for the requested platform. */ export function buildNavigationUrl( platform: NavigationPlatform, stops: NavigationStop[] ): NavigationExportResult { return platform === "google-maps" ? buildGoogleMapsUrl(stops) : buildAppleMapsUrl(stops); } export { isError as isNavigationExportError };