import * as Location from 'expo-location'; import { GPSInfo } from '../types'; export async function requestLocationPermissions(): Promise { try { const { status } = await Location.requestForegroundPermissionsAsync(); return status === 'granted'; } catch (error) { console.error('Error requesting location permissions:', error); return false; } } // The OS permission screen takes the window focus for a moment and the camera // session goes down with it, so a capture that asks for location comes back // "Camera is closed" — one ask per shot turned a burst into failed frames. // Check the status (no UI, cheap) and raise the dialog at most once per run. let locationAsked = false; async function ensureLocationPermission(): Promise { try { if ((await Location.getForegroundPermissionsAsync()).granted) return true; if (locationAsked) return false; locationAsked = true; return (await Location.requestForegroundPermissionsAsync()).status === 'granted'; } catch (error) { console.error('Error requesting location permissions:', error); return false; } } export async function getCurrentGPS(): Promise { try { const hasPermission = await ensureLocationPermission(); if (!hasPermission) return null; const location = await Location.getCurrentPositionAsync({ accuracy: Location.Accuracy.Balanced, }); const { latitude, longitude } = location.coords; let locality = 'UNKNOWN LOCATION'; let country = ''; try { const geocode = await Location.reverseGeocodeAsync({ latitude, longitude }); if (geocode && geocode.length > 0) { const address = geocode[0]; // Combine city, region, street or subregion to form a retro looking place string locality = localityName(address) || 'LOCAL REGION'; country = (address.country || '').toUpperCase(); } } catch (e) { console.warn('Reverse geocoding failed, using coordinates only:', e); } return { latitude, longitude, locality, country, timestamp: location.timestamp, }; } catch (error) { console.error('Error fetching GPS info:', error); return null; } } // The one place-name picker, shared so the camera stamp and a photo's stamp can // never disagree. A Vietnamese geocoder answers with the commune/ward in // subregion (post-2025 two-level administration: the commune is the district- // level name) and the province/city in region, while city and district come // back null, so the stamp reads "COMMUNE, CITY" instead of a bare city. export function localityName(address: Location.LocationGeocodedAddress): string | null { const district = address.subregion || address.district; const city = address.city || address.region; // Same name in both slots (one field echoed twice) must not print twice. const parts = district && district !== city ? [district, city] : [district || city]; const name = parts.filter(Boolean).join(', '); return name ? name.toUpperCase() : null; } export async function reverseGeocode(latitude: number, longitude: number): Promise { // Foreground permission is not optional here: expo-location's Android // geocoder throws LocationUnauthorizedException without it. A photo picked // before the camera ever asked for location therefore resolved to no name at // all and the stamp fell back to "STREET VIEW" — so make sure it is held // before every call (same once-per-run ask as getCurrentGPS). await ensureLocationPermission(); try { const geo = await Location.reverseGeocodeAsync({ latitude, longitude }); if (geo && geo.length > 0) return localityName(geo[0]); } catch (e) { console.warn('Reverse geocode failed:', e); } return null; }