const { Client } = require('@googlemaps/google-maps-services-js'); class GoogleMapsService { constructor() { this.client = new Client({}); this.apiKey = process.env.GOOGLE_MAPS_API_KEY; if (!this.apiKey) { console.error('❌ Google Maps API key not configured in environment variables'); } else { console.log('✅ Google Maps service initialized'); } } /** * Get autocomplete predictions for places */ async getPlacesAutocomplete(input, options = {}) { if (!this.apiKey) { throw new Error('Google Maps API key not configured'); } if (!input || input.trim().length < 2) { return { predictions: [] }; } try { const params = { key: this.apiKey, input: input.trim(), types: options.types || 'address', language: options.language || 'en', ...options }; // Add session token if provided if (options.sessionToken) { params.sessiontoken = options.sessionToken; } // Add component restrictions (e.g., country) if (options.componentRestrictions) { params.components = Object.entries(options.componentRestrictions) .map(([key, value]) => `${key}:${value}`) .join('|'); } const response = await this.client.placeAutocomplete({ params, timeout: 5000 }); if (response.data.status === 'OK') { return { predictions: response.data.predictions.map(prediction => ({ placeId: prediction.place_id, description: prediction.description, types: prediction.types, mainText: prediction.structured_formatting.main_text, secondaryText: prediction.structured_formatting.secondary_text || '' })) }; } else { console.error('Places Autocomplete API error:', response.data.status, response.data.error_message); return { predictions: [], error: this.getErrorMessage(response.data.status), status: response.data.status }; } } catch (error) { console.error('Places Autocomplete service error:', error.message); throw new Error('Failed to fetch place predictions'); } } /** * Get detailed information about a place */ async getPlaceDetails(placeId, options = {}) { if (!this.apiKey) { throw new Error('Google Maps API key not configured'); } if (!placeId) { throw new Error('Place ID is required'); } try { const params = { key: this.apiKey, place_id: placeId, fields: [ 'address_components', 'formatted_address', 'geometry', 'place_id' ], language: options.language || 'en' }; // Add session token if provided if (options.sessionToken) { params.sessiontoken = options.sessionToken; } const response = await this.client.placeDetails({ params, timeout: 5000 }); if (response.data.status === 'OK' && response.data.result) { const place = response.data.result; const addressComponents = {}; // Parse address components if (place.address_components) { place.address_components.forEach(component => { const types = component.types; if (types.includes('street_number')) { addressComponents.streetNumber = component.long_name; } else if (types.includes('route')) { addressComponents.route = component.long_name; } else if (types.includes('locality')) { addressComponents.locality = component.long_name; } else if (types.includes('administrative_area_level_1')) { addressComponents.administrativeAreaLevel1 = component.short_name; addressComponents.administrativeAreaLevel1Long = component.long_name; } else if (types.includes('postal_code')) { addressComponents.postalCode = component.long_name; } else if (types.includes('country')) { addressComponents.country = component.short_name; } }); } return { placeId: place.place_id, formattedAddress: place.formatted_address, addressComponents, geometry: { latitude: place.geometry?.location?.lat || 0, longitude: place.geometry?.location?.lng || 0 } }; } else { console.error('Place Details API error:', response.data.status, response.data.error_message); throw new Error(this.getErrorMessage(response.data.status)); } } catch (error) { console.error('Place Details service error:', error.message); throw error; } } /** * Geocode an address to get coordinates */ async geocodeAddress(address, options = {}) { if (!this.apiKey) { throw new Error('Google Maps API key not configured'); } if (!address || !address.trim()) { throw new Error('Address is required for geocoding'); } try { const params = { key: this.apiKey, address: address.trim(), language: options.language || 'en' }; // Add component restrictions (e.g., country) if (options.componentRestrictions) { params.components = Object.entries(options.componentRestrictions) .map(([key, value]) => `${key}:${value}`) .join('|'); } // Add bounds if provided if (options.bounds) { params.bounds = options.bounds; } const response = await this.client.geocode({ params, timeout: 5000 }); if (response.data.status === 'OK' && response.data.results.length > 0) { const result = response.data.results[0]; return { latitude: result.geometry.location.lat, longitude: result.geometry.location.lng, formattedAddress: result.formatted_address, placeId: result.place_id }; } else { console.error('Geocoding API error:', response.data.status, response.data.error_message); return { error: this.getErrorMessage(response.data.status), status: response.data.status }; } } catch (error) { console.error('Geocoding service error:', error.message); throw new Error('Failed to geocode address'); } } /** * Get human-readable error message for Google Maps API status codes */ getErrorMessage(status) { const errorMessages = { 'ZERO_RESULTS': 'No results found for this query', 'OVER_QUERY_LIMIT': 'API quota exceeded. Please try again later', 'REQUEST_DENIED': 'API request denied. Check API key configuration', 'INVALID_REQUEST': 'Invalid request parameters', 'UNKNOWN_ERROR': 'Server error. Please try again', 'NOT_FOUND': 'The specified place was not found' }; return errorMessages[status] || `Google Maps API error: ${status}`; } /** * Check if the service is properly configured */ isConfigured() { return !!this.apiKey; } } // Export singleton instance module.exports = new GoogleMapsService();