UNPKG

expo-osm-sdk

Version:

OpenStreetMap component for React Native with Expo

368 lines 13.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.calculateStraightLineDistance = exports.formatDistance = exports.formatDuration = exports.getRouteEstimate = exports.calculateSimpleRoute = exports.calculateRoute = void 0; /** * OSRM (Open Source Routing Machine) Service * * Provides routing and navigation using OpenStreetMap data via OSRM API. * Free service with reasonable usage limits for development and small apps. * For production use, consider hosting your own OSRM instance. */ // Default OSRM demo server const OSRM_BASE_URL = 'https://router.project-osrm.org'; // Rate limiting for demo server const REQUEST_DELAY = 300; // 300ms between requests (safer rate limit) let lastRequestTime = 0; /** * Rate-limited fetch for OSRM demo server */ const rateLimitedFetch = async (url) => { const now = Date.now(); const timeSinceLastRequest = now - lastRequestTime; if (timeSinceLastRequest < REQUEST_DELAY) { const delay = REQUEST_DELAY - timeSinceLastRequest; await new Promise(resolve => setTimeout(resolve, delay)); } lastRequestTime = Date.now(); const response = await fetch(url, { headers: { 'User-Agent': 'expo-osm-sdk/1.0 (https://github.com/mapdevsaikat/expo-osm-sdk)', }, }); if (!response.ok) { throw new Error(`OSRM request failed: ${response.status} ${response.statusText}`); } return response; }; /** * Handle transit routing by falling back to walking + public transport estimation */ const calculateTransitRoute = async (waypoints, options = {}) => { // For transit, we fallback to walking route with adjusted timing // In a real implementation, you would integrate with public transit APIs console.log('🚌 Transit mode: Using walking route with public transport estimation'); const walkingRoutes = await calculateNativeOSRMRoute(waypoints, { ...options, profile: 'walking' }); if (walkingRoutes.length === 0) { throw new Error('No transit route found'); } // Adjust timing to simulate public transport const baseRoute = walkingRoutes[0]; if (!baseRoute) { throw new Error('No base route found for transit calculation'); } const transitRoute = { coordinates: baseRoute.coordinates, distance: baseRoute.distance, duration: Math.max(baseRoute.duration * 0.6, baseRoute.duration - 300), // Faster than walking steps: baseRoute.steps.map(step => ({ ...step, instruction: step.instruction.replace(/walk/gi, 'Take public transport').replace(/continue/gi, 'Continue on transit') })) }; return [transitRoute]; }; /** * Calculate route using native OSRM profiles */ const calculateNativeOSRMRoute = async (waypoints, options = {}) => { if (waypoints.length < 2) { throw new Error('At least 2 waypoints are required for routing'); } // Validate coordinates waypoints.forEach((coord, index) => { console.log(`🔍 Validating waypoint ${index}:`, coord); if (!isValidCoordinate(coord)) { console.error(`❌ Invalid coordinate at waypoint ${index}:`, coord); throw new Error(`Invalid coordinate at waypoint ${index}: ${JSON.stringify(coord)}`); } }); const { profile = 'driving', alternatives = false, steps = true, geometries = 'polyline', overview = 'full', continue_straight = false } = options; // Ensure we have a native OSRM profile const nativeProfile = profile; // Build coordinate string (longitude,latitude format for OSRM) const coordsString = waypoints .map(coord => `${coord.longitude},${coord.latitude}`) .join(';'); const params = new URLSearchParams({ alternatives: alternatives.toString(), steps: steps.toString(), geometries, overview, continue_straight: continue_straight.toString() }); try { const url = `${OSRM_BASE_URL}/route/v1/${nativeProfile}/${coordsString}?${params.toString()}`; console.log('🚗 OSRM route request:', nativeProfile, waypoints.length, 'waypoints'); console.log('📍 OSRM coordinates:', coordsString); console.log('🔗 OSRM URL:', url); const response = await rateLimitedFetch(url); if (!response.ok) { const errorText = await response.text(); console.error('❌ OSRM HTTP Error:', response.status, response.statusText); console.error('❌ OSRM Error Response:', errorText); throw new Error(`OSRM request failed: ${response.status} ${response.statusText}`); } const data = await response.json(); if (data.code !== 'Ok') { throw new Error(`OSRM error: ${data.message || data.code}`); } if (!data.routes || data.routes.length === 0) { throw new Error('No routes found'); } console.log('📍 Found', data.routes.length, 'route(s) for profile:', nativeProfile); // Convert OSRM routes to our Route interface return data.routes.map(osrmRoute => convertOSRMRoute(osrmRoute, geometries)); } catch (error) { console.error('❌ OSRM routing failed:', error); throw new Error(`Routing failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } }; /** * Calculate route between multiple waypoints * * @param waypoints - Array of coordinates (minimum 2 points) * @param options - Routing options * @returns Promise<Route[]> */ const calculateRoute = async (waypoints, options = {}) => { const { profile = 'driving' } = options; // Handle transit mode separately if (profile === 'transit') { return calculateTransitRoute(waypoints, options); } // Handle native OSRM profiles return calculateNativeOSRMRoute(waypoints, options); }; exports.calculateRoute = calculateRoute; /** * Calculate simple point-to-point route * * @param from - Start coordinate * @param to - End coordinate * @param profile - Routing profile * @returns Promise<Route> */ const calculateSimpleRoute = async (from, to, profile = 'driving') => { const routes = await (0, exports.calculateRoute)([from, to], { profile, alternatives: false }); if (routes.length === 0) { throw new Error('No route found'); } return routes[0]; }; exports.calculateSimpleRoute = calculateSimpleRoute; /** * Get route duration estimate (without full route calculation) * * @param from - Start coordinate * @param to - End coordinate * @param profile - Routing profile * @returns Promise<{distance: number, duration: number}> */ const getRouteEstimate = async (from, to, profile = 'driving') => { const route = await (0, exports.calculateSimpleRoute)(from, to, profile); return { distance: route.distance, duration: route.duration }; }; exports.getRouteEstimate = getRouteEstimate; /** * Convert OSRM route to our Route interface */ function convertOSRMRoute(osrmRoute, geometryType = 'polyline') { // Decode geometry based on type let coordinates; if (geometryType === 'geojson') { // GeoJSON geometry coordinates = osrmRoute.geometry.coordinates.map((coord) => ({ latitude: coord[1], longitude: coord[0] })); } else { // Polyline encoded geometry (default for OSRM) if (typeof osrmRoute.geometry === 'string') { coordinates = decodePolyline(osrmRoute.geometry); } else { console.warn('⚠️ Unexpected geometry format, falling back to waypoints'); // Fallback: extract coordinates from waypoints/steps coordinates = []; osrmRoute.legs.forEach(leg => { if (leg.steps) { leg.steps.forEach(step => { coordinates.push({ latitude: step.maneuver.location[1], longitude: step.maneuver.location[0] }); }); } }); } } // Convert steps from all legs const steps = []; osrmRoute.legs.forEach(leg => { if (leg.steps) { leg.steps.forEach(osrmStep => { steps.push({ instruction: generateInstruction(osrmStep.maneuver), distance: osrmStep.distance, duration: osrmStep.duration, coordinate: { latitude: osrmStep.maneuver.location[1], longitude: osrmStep.maneuver.location[0] } }); }); } }); return { coordinates, distance: osrmRoute.distance, duration: osrmRoute.duration, steps }; } /** * Generate human-readable instruction from OSRM maneuver */ function generateInstruction(maneuver) { const { type, modifier } = maneuver; const instructions = { 'depart': 'Head out', 'turn': modifier ? `Turn ${modifier}` : 'Turn', 'new name': 'Continue', 'continue': 'Continue', 'merge': 'Merge', 'on ramp': 'Take the ramp', 'off ramp': 'Take the exit', 'fork': 'Keep to the fork', 'end of road': modifier ? `Turn ${modifier} at the end of the road` : 'Continue at the end of the road', 'use lane': 'Use the lane', 'roundabout': 'Enter the roundabout', 'roundabout turn': 'Take the roundabout', 'exit roundabout': 'Exit the roundabout', 'arrive': 'You have arrived at your destination' }; return instructions[type] || `${type} ${modifier || ''}`.trim(); } /** * Improved polyline decoder with better error handling */ function decodePolyline(encoded) { if (!encoded || typeof encoded !== 'string') { console.warn('⚠️ Invalid polyline string:', encoded); return []; } const coordinates = []; let index = 0; let lat = 0; let lng = 0; try { while (index < encoded.length) { let byte = 0; let shift = 0; let result = 0; do { if (index >= encoded.length) break; byte = encoded.charCodeAt(index++) - 63; result |= (byte & 0x1F) << shift; shift += 5; } while (byte >= 0x20); const deltaLat = (result & 1) !== 0 ? ~(result >> 1) : result >> 1; lat += deltaLat; shift = 0; result = 0; do { if (index >= encoded.length) break; byte = encoded.charCodeAt(index++) - 63; result |= (byte & 0x1F) << shift; shift += 5; } while (byte >= 0x20); const deltaLng = (result & 1) !== 0 ? ~(result >> 1) : result >> 1; lng += deltaLng; coordinates.push({ latitude: lat / 1e5, longitude: lng / 1e5 }); } } catch (error) { console.error('❌ Polyline decoding error:', error); return []; } return coordinates; } /** * Validate coordinate values */ function isValidCoordinate(coord) { return (coord && typeof coord.latitude === 'number' && typeof coord.longitude === 'number' && coord.latitude >= -90 && coord.latitude <= 90 && coord.longitude >= -180 && coord.longitude <= 180); } /** * Format duration in human-readable format */ const formatDuration = (seconds) => { const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const remainingSeconds = Math.floor(seconds % 60); if (hours > 0) { return `${hours}h ${minutes}m`; } else if (minutes > 0) { return `${minutes}m ${remainingSeconds}s`; } else { return `${remainingSeconds}s`; } }; exports.formatDuration = formatDuration; /** * Format distance in human-readable format */ const formatDistance = (meters) => { if (meters >= 1000) { const km = (meters / 1000).toFixed(1); return `${km} km`; } else { return `${Math.round(meters)} m`; } }; exports.formatDistance = formatDistance; /** * Calculate straight-line distance between two coordinates (fallback) */ const calculateStraightLineDistance = (from, to) => { const R = 6371000; // Earth's radius in meters const φ1 = toRadians(from.latitude); const φ2 = toRadians(to.latitude); const Δφ = toRadians(to.latitude - from.latitude); const Δλ = toRadians(to.longitude - from.longitude); const a = Math.sin(Δφ / 2) * Math.sin(Δφ / 2) + Math.cos1) * Math.cos2) * Math.sin(Δλ / 2) * Math.sin(Δλ / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; }; exports.calculateStraightLineDistance = calculateStraightLineDistance; /** * Convert degrees to radians */ function toRadians(degrees) { return degrees * (Math.PI / 180); } //# sourceMappingURL=osrm.js.map