import { Platform, PermissionsAndroid, Alert, Linking } from 'react-native'; import Geolocation from 'react-native-geolocation-service'; import type { GeoPosition, GeoError } from 'react-native-geolocation-service'; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- export interface LatLng { latitude: number; longitude: number; } export interface GeocodeResult { address: string; city: string; state: string; postalCode: string; } export interface LocationResult { coords: LatLng; address: string; city: string; state: string; postalCode: string; } // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- const GOOGLE_MAPS_API_KEY = 'AIzaSyDKaKVyQCmSpHMXcMkNCccSXpILpuvwsVM'; /** Default region (Bengaluru) used as a fallback before the user's location loads. */ export const DEFAULT_LOCATION: LatLng = { latitude: 12.9352, longitude: 77.6245, }; // --------------------------------------------------------------------------- // Permission helpers // --------------------------------------------------------------------------- /** * Request location permission from the user. * Returns `true` if permission was granted, `false` otherwise. */ export const requestLocationPermission = async (): Promise => { if (Platform.OS === 'ios') { try { const result = await Geolocation.requestAuthorization('whenInUse'); return result === 'granted'; } catch { return false; } } // Android — check first to avoid showing the dialog when already granted try { const alreadyGranted = await PermissionsAndroid.check( PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION, ); if (alreadyGranted) { return true; } const granted = await PermissionsAndroid.request( PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION, { title: 'Location Permission', message: 'This app needs access to your location to set it on the map.', buttonNeutral: 'Ask Me Later', buttonNegative: 'Cancel', buttonPositive: 'OK', }, ); console.log('granted', granted); return granted === PermissionsAndroid.RESULTS.GRANTED; } catch { return false; } }; /** * Show an alert when the user denies location permission, with an option * to open app settings. */ export const showPermissionDeniedAlert = (): void => { Alert.alert( 'Location Permission Required', 'Please enable location permission in your device settings to use this feature.', [ { text: 'Cancel', style: 'cancel' }, { text: 'Open Settings', onPress: () => Linking.openSettings() }, ], ); }; // --------------------------------------------------------------------------- // Position helpers // --------------------------------------------------------------------------- /** * Get the device's current GPS position. * Wraps the callback-based Geolocation API in a Promise. */ /** * Wraps Geolocation.getCurrentPosition in a Promise. * Tries high-accuracy first; falls back to low-accuracy on failure. */ const getPositionOnce = (highAccuracy: boolean): Promise => new Promise((resolve, reject) => { Geolocation.getCurrentPosition( (position: GeoPosition) => { resolve({ latitude: position.coords.latitude, longitude: position.coords.longitude, }); }, (error: GeoError) => { console.log('Geolocation error (highAccuracy=' + highAccuracy + '):', error); reject(error); }, { enableHighAccuracy: highAccuracy, timeout: highAccuracy ? 5000 : 10000, maximumAge: 10000, forceRequestLocation: false, // avoid native dialog that can crash showLocationDialog: true, }, ); }); export const getCurrentPosition = async (): Promise => { console.log('getCurrentPosition called'); try { // First attempt: high accuracy (GPS) return await getPositionOnce(true); } catch (highAccuracyError) { console.warn('High-accuracy location failed, retrying with low accuracy…', highAccuracyError); try { // Fallback: network / cell-tower accuracy return await getPositionOnce(false); } catch (lowAccuracyError) { console.error('Failed to get current position:', lowAccuracyError); throw lowAccuracyError; } } }; // --------------------------------------------------------------------------- // Geocoding // --------------------------------------------------------------------------- /** * Reverse-geocode a lat/lng pair into a human-readable address string * using the Google Maps Geocoding API. */ export const reverseGeocode = async (coords: LatLng): Promise => { try { const url = `https://maps.googleapis.com/maps/api/geocode/json` + `?latlng=${coords.latitude},${coords.longitude}` + `&key=${GOOGLE_MAPS_API_KEY}`; const response = await fetch(url); const data = await response.json(); if (data.status === 'OK' && data.results.length > 0) { // Extract city, state, postalCode let city = ''; let state = ''; let postalCode = ''; const result = data.results[0]; result.address_components.forEach((component: any) => { if (component.types.includes('locality') || component.types.includes('sublocality')) { city = city || component.long_name; } if (component.types.includes('administrative_area_level_1')) { state = component.long_name; } if (component.types.includes('postal_code')) { postalCode = component.long_name; } }); // Return the most specific formatted address and extracted details return { address: result.formatted_address, city, state, postalCode, }; } console.warn('Google Maps Geocoding failed:', data.status, data.error_message || ''); // Fallback to OpenStreetMap Nominatim const osmUrl = `https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${coords.latitude}&lon=${coords.longitude}`; const osmResponse = await fetch(osmUrl, { headers: { 'User-Agent': 'SGDeliveryCustomerApp/1.0', }, }); const osmData = await osmResponse.json(); if (osmData && osmData.display_name) { return { address: osmData.display_name, city: osmData.address?.city || osmData.address?.town || osmData.address?.village || osmData.address?.county || '', state: osmData.address?.state || '', postalCode: osmData.address?.postcode || '', }; } return { address: 'Address not found', city: '', state: '', postalCode: '' }; } catch (error) { console.log('reverseGeocode error', error); try { // Fallback to OpenStreetMap Nominatim on catch const osmUrl = `https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${coords.latitude}&lon=${coords.longitude}`; const osmResponse = await fetch(osmUrl, { headers: { 'User-Agent': 'SGDeliveryCustomerApp/1.0', }, }); const osmData = await osmResponse.json(); if (osmData && osmData.display_name) { return { address: osmData.display_name, city: osmData.address?.city || osmData.address?.town || osmData.address?.village || osmData.address?.county || '', state: osmData.address?.state || '', postalCode: osmData.address?.postcode || '', }; } } catch (osmError) { console.log('OSM fallback error', osmError); } return { address: 'Unable to fetch address', city: '', state: '', postalCode: '' }; } }; // --------------------------------------------------------------------------- // Combined helper // --------------------------------------------------------------------------- /** * All-in-one helper: request permissions → get position → reverse geocode. * Returns both the coordinates and the human-readable address. * * Throws if permission is denied or position cannot be obtained. */ export const getCurrentLocationWithAddress = async (): Promise => { const hasPermission = await requestLocationPermission(); console.log('hasPermission', hasPermission); if (!hasPermission) { // Alert shown by the caller so it doesn't fire inside a Promise chain // on mount (which can cause a crash on some Android versions). throw new Error('Location permission denied'); } const coords = await getCurrentPosition(); const geocodeResult = await reverseGeocode(coords); console.log('coords', coords); console.log('address', geocodeResult.address); return { coords, address: geocodeResult.address, city: geocodeResult.city, state: geocodeResult.state, postalCode: geocodeResult.postalCode }; }; // --------------------------------------------------------------------------- // Route & Distance helpers // --------------------------------------------------------------------------- /** * Decodes a Google Maps encoded polyline string. * @param encoded - The encoded polyline string. */ export const decodePolyline = ( encoded: string, ): LatLng[] => { const points: LatLng[] = []; let index = 0; const len = encoded.length; let lat = 0; let lng = 0; while (index < len) { let b; let shift = 0; let result = 0; do { b = encoded.charCodeAt(index++) - 63; result |= (b & 0x1f) << shift; shift += 5; } while (b >= 0x20); const dlat = result & 1 ? ~(result >> 1) : result >> 1; lat += dlat; shift = 0; result = 0; do { b = encoded.charCodeAt(index++) - 63; result |= (b & 0x1f) << shift; shift += 5; } while (b >= 0x20); const dlng = result & 1 ? ~(result >> 1) : result >> 1; lng += dlng; points.push({ latitude: lat / 1e5, longitude: lng / 1e5, }); } return points; }; /** * Fetch directions coordinates between origin and destination using the Google Maps Directions API. * Falls back to a straight line [origin, destination] if the request fails. */ export const getRouteDirections = async ( origin: LatLng, destination: LatLng, ): Promise => { // 1. Try Google Maps Directions API first try { const url = `https://maps.googleapis.com/maps/api/directions/json` + `?origin=${origin.latitude},${origin.longitude}` + `&destination=${destination.latitude},${destination.longitude}` + `&key=${GOOGLE_MAPS_API_KEY}`; const response = await fetch(url); const data = await response.json(); if (data.status === 'OK' && data.routes && data.routes.length > 0) { console.log('Successfully fetched Google Directions'); return decodePolyline(data.routes[0].overview_polyline.points); } else { console.warn( 'Google Maps Directions status not OK:', data.status, data.error_message || '', ); throw new Error(`Google Directions status: ${data.status}`); } } catch (error) { console.error( 'Error fetching Google Directions, trying OSRM fallback:', error, ); } // 2. Fallback to OpenStreetMap OSRM API (completely free and no key required) try { const osmUrl = `https://router.project-osrm.org/route/v1/driving/${origin.longitude},${origin.latitude};${destination.longitude},${destination.latitude}?overview=full&geometries=geojson`; const response = await fetch(osmUrl); const data = await response.json(); if (data.code === 'Ok' && data.routes && data.routes.length > 0) { console.log('Successfully fetched OSRM Directions'); return data.routes[0].geometry.coordinates.map( (point: [number, number]) => ({ latitude: point[1], longitude: point[0], }), ); } else { console.warn('OSM OSRM Routing status not Ok:', data.code); } } catch (error) { console.error('Error fetching OSRM directions:', error); } // 3. Ultimate Fallback to straight line return [origin, destination]; }; /** * Calculate the distance between two coordinates in kilometers using the Haversine formula. */ export const getHaversineDistance = ( coords1: LatLng, coords2: LatLng, ): number => { const toRad = (value: number) => (value * Math.PI) / 180; const R = 6371; // Earth's radius in km const dLat = toRad(coords2.latitude - coords1.latitude); const dLon = toRad(coords2.longitude - coords1.longitude); const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(toRad(coords1.latitude)) * Math.cos(toRad(coords2.latitude)) * Math.sin(dLon / 2) * Math.sin(dLon / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; // Distance in km };