MVP Build
This commit is contained in:
112
backend/src/features/stations/external/google-maps/google-maps.client.ts
vendored
Normal file
112
backend/src/features/stations/external/google-maps/google-maps.client.ts
vendored
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* @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<Station[]> {
|
||||
const cacheKey = `stations:${latitude.toFixed(4)},${longitude.toFixed(4)},${radius}`;
|
||||
|
||||
try {
|
||||
// Check cache
|
||||
const cached = await cacheService.get<Station[]>(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<GooglePlacesResponse>(
|
||||
`${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();
|
||||
Reference in New Issue
Block a user