sg-delivery-customer/app/services/locationServices.ts

361 lines
11 KiB
TypeScript

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 LocationResult {
coords: LatLng;
address: 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<boolean> => {
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<LatLng> =>
new Promise<LatLng>((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<LatLng> => {
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<string> => {
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) {
// Return the most specific formatted address.
return data.results[0].formatted_address;
}
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 osmData.display_name;
}
return 'Address not found';
} 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 osmData.display_name;
}
} catch (osmError) {
console.log('OSM fallback error', osmError);
}
return 'Unable to fetch address';
}
};
// ---------------------------------------------------------------------------
// 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<LocationResult> => {
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 address = await reverseGeocode(coords);
console.log('coords', coords);
console.log('address', address);
return { coords, address };
};
// ---------------------------------------------------------------------------
// 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<LatLng[]> => {
// 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
};