Files
rentall-app/backend/services/googleMapsService.js
2025-09-09 22:49:55 -04:00

241 lines
7.1 KiB
JavaScript

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();