Google maps integration
This commit is contained in:
241
backend/services/googleMapsService.js
Normal file
241
backend/services/googleMapsService.js
Normal file
@@ -0,0 +1,241 @@
|
||||
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();
|
||||
Reference in New Issue
Block a user