241 lines
7.1 KiB
JavaScript
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(); |