UNPKG

google-places-core

Version:

A lightweight Google Places API logic library.

417 lines (399 loc) 16.2 kB
'use strict'; /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */ function __rest(s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; const debounce = (func, delay) => { let timeoutId; const debouncedFunction = (...args) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => func(...args), delay); }; debouncedFunction.cancel = () => { clearTimeout(timeoutId); }; return debouncedFunction; }; class GooglePlacesManager { constructor(service, debounceTime = 300) { this.state = { predictions: [], isLoading: false, error: null, }; this.subscribers = new Set(); this.fetchPredictionsInternal = async (input) => { if (!input) { this.setState({ predictions: [], isLoading: false, error: null }); return; } this.setState({ isLoading: true, error: null }); try { const predictions = await this.placesService.fetchPredictions(input); this.setState({ predictions, isLoading: false }); } catch (error) { console.error('Error fetching predictions:', error); this.setState({ predictions: [], isLoading: false, error }); } }; this.placesService = service; this.debouncedGetPredictions = debounce(this.fetchPredictionsInternal, debounceTime); } setState(newState) { this.state = Object.assign(Object.assign({}, this.state), newState); this.subscribers.forEach((callback) => callback(this.state)); } updateSearchInput(input) { this.debouncedGetPredictions(input); } async getPlaceDetails(placeId) { try { return await this.placesService.fetchPlaceDetails(placeId); } catch (error) { console.error('Error fetching place details:', error); this.setState({ error }); throw error; } } subscribe(callback) { this.subscribers.add(callback); callback(this.state); return () => { this.subscribers.delete(callback); }; } getState() { return Object.assign({}, this.state); } destroy() { this.debouncedGetPredictions.cancel(); this.subscribers.clear(); } } const fieldsPlaceDetails = [ 'displayName', 'formattedAddress', 'location', 'viewport', 'addressComponents', 'types', ]; class Helpers { constructor(apiKey) { this.apiKey = apiKey; } checkBrowserEnvironment() { return typeof window !== 'undefined' && typeof document !== 'undefined'; } isLoaded() { var _a, _b; return typeof window !== 'undefined' && typeof window.google !== 'undefined' && typeof ((_b = (_a = window.google) === null || _a === void 0 ? void 0 : _a.maps) === null || _b === void 0 ? void 0 : _b.importLibrary) === 'function'; } isScriptInjected() { return Array.from(document.scripts).some((s) => s.src.includes('maps.googleapis.com/maps/api/js')); } injectScript() { return new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = `https://maps.googleapis.com/maps/api/js?key=${this.apiKey}&libraries=places`; script.async = true; script.defer = true; script.onload = () => resolve(); script.onerror = () => reject(new Error('Failed to load Google Maps API')); document.head.appendChild(script); }); } waitForLoad() { return new Promise((resolve) => { const check = () => this.isLoaded() ? resolve() : setTimeout(check, 100); setTimeout(() => resolve(), 10000); // Timeout after 10s check(); }); } } class WebPlaceDetails extends Helpers { constructor(apiKey) { if (!apiKey) { throw new Error('GooglePlacesWebService: API Key must be provided.'); } super(apiKey); this.initializationPromise = null; this.placesLibrary = null; this.isBrowser = this.checkBrowserEnvironment(); } async fetchPlaceDetails(placeId) { await this.ensureInitialized(); return await this.getPlaceDetails(placeId); } // Initialization async ensureInitialized() { if (!this.isBrowser) { throw new Error('GooglePlacesWebService: Browser environment required'); } if (this.initializationPromise) { return this.initializationPromise; } this.initializationPromise = this.initialize(); return this.initializationPromise; } async initialize() { await this.injectScript(); await this.waitForLoad(); await this.loadPlacesLibrary(); } async loadPlacesLibrary() { if (!this.isLoaded()) { throw new Error('Google Maps API not available'); } this.placesLibrary = await window.google.maps.importLibrary('places'); } // Place Details using new JavaScript SDK async getPlaceDetails(placeId) { if (!this.placesLibrary) { throw new Error('Places library not initialized'); } const { Place } = this.placesLibrary; const place = new Place({ id: placeId, requestedLanguage: 'en', }); // Fetch required fields await place.fetchFields({ fields: fieldsPlaceDetails, }); return this.transformPlaceDetails(place); } // Transformation methods transformPlaceDetails(place) { var _a, _b, _c, _d; const viewport = JSON.stringify(place.viewport); return { formatted_address: place.formattedAddress || '', geometry: { location: { lat: ((_a = place.location) === null || _a === void 0 ? void 0 : _a.lat()) || 0, lng: ((_b = place.location) === null || _b === void 0 ? void 0 : _b.lng()) || 0, }, viewport: JSON.parse(viewport), }, name: ((_c = place.displayName) === null || _c === void 0 ? void 0 : _c.text) || '', place_id: place.id || '', address_components: ((_d = place.addressComponents) === null || _d === void 0 ? void 0 : _d.map((component) => ({ long_name: component.longText, short_name: component.shortText, types: component.types, }))) || [], types: place.types || [], }; } } class NativePlaceDetails { constructor(apiKey, config) { this.fetchPlaceDetails = async (placeId) => { const response = await fetch(`https://places.googleapis.com/v1/places/${placeId}?fields=${fieldsPlaceDetails.join(',')}&key=${this.apiKey}`); if (!response.ok) { const errorData = await response.json(); console.error('Google Places Details Error:', errorData); throw new Error(`Failed to fetch place details: ${errorData.error_message || response.statusText}`); } const data = await response.json(); return this.transformPlaceDetails(data); }; this.apiKey = apiKey; this.config = config || {}; } // Transformation methods transformPlaceDetails(place) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k; return { formatted_address: place.formattedAddress || '', geometry: { location: { lat: place.location.latitude || 0, lng: place.location.longitude || 0, }, viewport: { south: (_b = (_a = place.viewport) === null || _a === void 0 ? void 0 : _a.low) === null || _b === void 0 ? void 0 : _b.latitude, west: (_d = (_c = place.viewport) === null || _c === void 0 ? void 0 : _c.low) === null || _d === void 0 ? void 0 : _d.longitude, north: (_f = (_e = place.viewport) === null || _e === void 0 ? void 0 : _e.high) === null || _f === void 0 ? void 0 : _f.latitude, east: (_h = (_g = place.viewport) === null || _g === void 0 ? void 0 : _g.high) === null || _h === void 0 ? void 0 : _h.longitude, }, }, name: ((_j = place.displayName) === null || _j === void 0 ? void 0 : _j.text) || '', place_id: place.id || '', address_components: ((_k = place.addressComponents) === null || _k === void 0 ? void 0 : _k.map((component) => ({ long_name: component.longText, short_name: component.shortText, types: component.types, }))) || [], types: place.types || [], }; } } class PlatformDetector { static detect() { // Check for browser environment const hasWindow = typeof window !== 'undefined'; const hasDocument = typeof document !== 'undefined'; const hasNavigator = typeof navigator !== 'undefined'; return hasWindow && hasDocument && hasNavigator ? 'web' : 'native'; } static isWeb() { return this.detect() === 'web'; } static isNative() { return this.detect() === 'native'; } } class PlaceDetailsStrategyFactory { static createStrategy(platform, apiKey, config) { switch (platform) { case 'web': return new WebPlaceDetails(apiKey); case 'native': return new NativePlaceDetails(apiKey, config); default: throw new Error(`Unsupported platform: ${platform}`); } } static createStrategyForEnvironment(apiKey, config) { const platform = PlatformDetector.detect(); return this.createStrategy(platform, apiKey, config); } } class GooglePlacesService { constructor(config) { if (!config.apiKey) { throw new Error('API Key must be provided.'); } this.apiKey = config.apiKey; this.config = config; this.enableLogging = config.enableLogging || false; // Create appropriate strategy based on platform this.placeDetailsStrategy = PlaceDetailsStrategyFactory.createStrategyForEnvironment(this.apiKey, { countryRestrictions: config.countryRestrictions, locationBias: config.locationBias, languageCode: config.languageCode, }); this.log('GooglePlacesService initialized for', PlatformDetector.detect(), 'platform'); } async fetchPredictions(input) { this.log('Fetching predictions for:', input); const response = await fetch('https://places.googleapis.com/v1/places:autocomplete', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Goog-Api-Key': this.apiKey, 'X-Goog-FieldMask': 'suggestions.placePrediction', }, body: JSON.stringify(Object.assign(Object.assign({ input, languageCode: this.config.languageCode || 'en' }, this.getRegionRestriction()), { locationBias: this.getLocationBias(), includedPrimaryTypes: ['establishment', 'geocode'] })), }); if (!response.ok) { const error = await this.handleApiError(response); throw new Error(`Failed to fetch predictions: ${error}`); } const data = await response.json(); return this.transformPredictions(data); } async fetchPlaceDetails(placeId) { this.log('Fetching place details for:', placeId); // Delegate to the appropriate strategy return this.placeDetailsStrategy.fetchPlaceDetails(placeId); } // Private helper methods getLocationBias() { const defaultBias = { circle: { center: { latitude: 9.082, longitude: 8.6753 }, radius: 50000, }, }; return this.config.locationBias ? { circle: { center: this.config.locationBias.center, radius: this.config.locationBias.radius, }, } : defaultBias; } getRegionRestriction() { var _a; if (!((_a = this.config.countryRestrictions) === null || _a === void 0 ? void 0 : _a.length)) return {}; return { includedRegionCodes: this.config.countryRestrictions }; } async handleApiError(response) { var _a; try { const errorData = await response.json(); this.log('API Error:', errorData); return ((_a = errorData.error) === null || _a === void 0 ? void 0 : _a.message) || response.statusText; } catch (_b) { return response.statusText; } } transformPredictions(data) { if (!data.suggestions) { this.log('No suggestions found'); return []; } return data.suggestions.map((suggestion) => { var _a, _b, _c, _d, _e; const prediction = suggestion.placePrediction; return { description: ((_a = prediction.text) === null || _a === void 0 ? void 0 : _a.text) || '', place_id: prediction.placeId || '', structured_formatting: { main_text: ((_c = (_b = prediction.structuredFormat) === null || _b === void 0 ? void 0 : _b.mainText) === null || _c === void 0 ? void 0 : _c.text) || '', secondary_text: ((_e = (_d = prediction.structuredFormat) === null || _d === void 0 ? void 0 : _d.secondaryText) === null || _e === void 0 ? void 0 : _e.text) || '', }, types: prediction.types || [], }; }); } log(...args) { if (this.enableLogging) { console.log('[GooglePlacesService]', ...args); } } } function createGooglePlacesManager(apiKey, options) { const _a = options || {}, { debounceTime } = _a, config = __rest(_a, ["debounceTime"]); const service = new GooglePlacesService(Object.assign(Object.assign({}, config), { apiKey })); return new GooglePlacesManager(service, debounceTime); } exports.GooglePlacesManager = GooglePlacesManager; exports.GooglePlacesService = GooglePlacesService; exports.createGooglePlacesManager = createGooglePlacesManager; //# sourceMappingURL=index.js.map