/** * @ai-summary Google Maps client for station discovery * @ai-context Searches for gas stations and caches results */ import axios from 'axios'; import { env } from '../../../../core/config/environment'; import { logger } from '../../../../core/logging/logger'; import { cacheService } from '../../../../core/config/redis'; import { GooglePlacesResponse, GooglePlace } from './google-maps.types'; import { Station } from '../../domain/stations.types'; export class GoogleMapsClient { private readonly apiKey = env.GOOGLE_MAPS_API_KEY; private readonly baseURL = 'https://maps.googleapis.com/maps/api/place'; private readonly cacheTTL = 3600; // 1 hour async searchNearbyStations( latitude: number, longitude: number, radius: number = 5000 ): Promise { const cacheKey = `stations:${latitude.toFixed(4)},${longitude.toFixed(4)},${radius}`; try { // Check cache const cached = await cacheService.get(cacheKey); if (cached) { logger.debug('Station search cache hit', { latitude, longitude }); return cached; } // Search Google Places logger.info('Searching Google Places for stations', { latitude, longitude, radius }); const response = await axios.get( `${this.baseURL}/nearbysearch/json`, { params: { location: `${latitude},${longitude}`, radius, type: 'gas_station', key: this.apiKey } } ); if (response.data.status !== 'OK' && response.data.status !== 'ZERO_RESULTS') { throw new Error(`Google Places API error: ${response.data.status}`); } // Transform results const stations = response.data.results.map(place => this.transformPlaceToStation(place, latitude, longitude) ); // Cache results await cacheService.set(cacheKey, stations, this.cacheTTL); return stations; } catch (error) { logger.error('Station search failed', { error, latitude, longitude }); throw error; } } private transformPlaceToStation(place: GooglePlace, searchLat: number, searchLng: number): Station { // Calculate distance from search point const distance = this.calculateDistance( searchLat, searchLng, place.geometry.location.lat, place.geometry.location.lng ); // Generate photo URL if available let photoUrl: string | undefined; if (place.photos && place.photos.length > 0) { photoUrl = `https://maps.googleapis.com/maps/api/place/photo?maxwidth=400&photo_reference=${place.photos[0].photo_reference}&key=${this.apiKey}`; } return { id: place.place_id, placeId: place.place_id, name: place.name, address: place.vicinity, latitude: place.geometry.location.lat, longitude: place.geometry.location.lng, distance, isOpen: place.opening_hours?.open_now, rating: place.rating, photoUrl }; } private calculateDistance(lat1: number, lon1: number, lat2: number, lon2: number): number { const R = 6371e3; // Earth's radius in meters const φ1 = lat1 * Math.PI / 180; const φ2 = lat2 * Math.PI / 180; const Δφ = (lat2 - lat1) * Math.PI / 180; const Δλ = (lon2 - lon1) * Math.PI / 180; const a = Math.sin(Δφ/2) * Math.sin(Δφ/2) + Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ/2) * Math.sin(Δλ/2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); return Math.round(R * c); // Distance in meters } } export const googleMapsClient = new GoogleMapsClient();