diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..bc1c728 --- /dev/null +++ b/.babelrc @@ -0,0 +1,6 @@ +{ + "presets": ["es2015"], + "plugins": [ + ["transform-object-rest-spread", { "useBuiltIns": true }] + ] +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7e6db21 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea/ +node_modules/ +.DS_Store diff --git a/dist/config.js b/dist/config.js new file mode 100644 index 0000000..6e6465e --- /dev/null +++ b/dist/config.js @@ -0,0 +1,27 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = init; +function init() { + if (!global.google || !global.google.maps) { + throw new Error('Please import google maps'); + } + + if (!/3\.\d\d\.\d+/.test(global.google.maps.version)) { + throw new Error('Please import a google maps version 3+'); + } + + return { + getGoogleAutocompleteService: function getGoogleAutocompleteService() { + return new global.google.maps.places.AutocompleteService(); + }, + getGooglePlacesService: function getGooglePlacesService() { + return new global.google.maps.places.PlacesService(document.createElement('div')); + }, + getGeocoder: function getGeocoder() { + return new global.google.maps.Geocoder(); + } + }; +} \ No newline at end of file diff --git a/dist/constants.js b/dist/constants.js new file mode 100644 index 0000000..e41561c --- /dev/null +++ b/dist/constants.js @@ -0,0 +1,53 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +var googleStatus = exports.googleStatus = {}; +var _google$maps$places$P = google.maps.places.PlacesServiceStatus; +googleStatus.ERROR = _google$maps$places$P.ERROR; +googleStatus.INVALID_REQUEST = _google$maps$places$P.INVALID_REQUEST; +googleStatus.OK = _google$maps$places$P.OK; +googleStatus.OVER_QUERY_LIMIT = _google$maps$places$P.OVER_QUERY_LIMIT; +googleStatus.REQUEST_DENIED = _google$maps$places$P.REQUEST_DENIED; +googleStatus.UNKNOWN_ERROR = _google$maps$places$P.UNKNOWN_ERROR; +googleStatus.ZERO_RESULTS = _google$maps$places$P.ZERO_RESULTS; +var status = exports.status = { + INVALID_INPUT: 'INVALID_INPUT', + NO_RESULTS: 'NO_RESULTS', + SUCCESS: 'SUCCESS', + PARTIAL_SUCCESS: 'PARTIAL_SUCCESS' +}; + +var defaultPlaceTypes = exports.defaultPlaceTypes = { + street_address: 'streetAddress', + route: 'route', + street_number: 'streetNumber', + neighborhood: 'neighborhood', + postal_code: 'postalCode', + sublocality: 'sublocality', + locality: 'locality', + administrative_area_level_1: 'administrativeAreaLevel1', + administrative_area_level_2: 'administrativeAreaLevel2', + administrative_area_level_3: 'administrativeAreaLevel3', + administrative_area_level_4: 'administrativeAreaLevel4', + administrative_area_level_5: 'administrativeAreaLevel5', + country: 'country' +}; + +var validSearchTypes = exports.validSearchTypes = { + geocode: 'geocode', + address: 'address', + establishment: 'establishment', + '(regions)': '(regions)', + '(cities)': '(cities)' +}; + +var validStrategies = exports.validStrategies = ['searchByPlaceId', 'searchByText', 'searchWithGeocoder']; + +var outputTypes = exports.outputTypes = { + street_address: [defaultPlaceTypes.streetAddress, defaultPlaceTypes.route, defaultPlaceTypes.streetNumber, defaultPlaceTypes.neighborhood, defaultPlaceTypes.postalCode, defaultPlaceTypes.sublocality, defaultPlaceTypes.locality, defaultPlaceTypes.administrativeAreaLevel1, defaultPlaceTypes.administrativeAreaLevel2, defaultPlaceTypes.administrativeAreaLevel3, defaultPlaceTypes.administrativeAreaLevel4, defaultPlaceTypes.administrativeAreaLevel5, defaultPlaceTypes.country], + postal_code: [defaultPlaceTypes.postalCode, defaultPlaceTypes.sublocality, defaultPlaceTypes.locality, defaultPlaceTypes.administrativeAreaLevel1, defaultPlaceTypes.administrativeAreaLevel2, defaultPlaceTypes.administrativeAreaLevel3, defaultPlaceTypes.administrativeAreaLevel4, defaultPlaceTypes.administrativeAreaLevel5, defaultPlaceTypes.country], + locality: [defaultPlaceTypes.locality, defaultPlaceTypes.administrativeAreaLevel1, defaultPlaceTypes.administrativeAreaLevel2, defaultPlaceTypes.administrativeAreaLevel3, defaultPlaceTypes.administrativeAreaLevel4, defaultPlaceTypes.administrativeAreaLevel5, defaultPlaceTypes.country], + administrative_area_level_1: [defaultPlaceTypes.administrativeAreaLevel1, defaultPlaceTypes.country] +}; \ No newline at end of file diff --git a/dist/helpers.js b/dist/helpers.js new file mode 100644 index 0000000..85fd543 --- /dev/null +++ b/dist/helpers.js @@ -0,0 +1,118 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.rejectByRegex = exports.pipeStrategies = exports.getSearchStrategies = exports.getRestrictions = exports.getPlaceTypes = exports.findPlaceByType = exports.emptyResults = exports.getSearchType = exports.getLatLong = undefined; + +var _constants = require('./constants'); + +function _toArray(arr) { return Array.isArray(arr) ? arr : Array.from(arr); } + +function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } + +var getLatLong = exports.getLatLong = function getLatLong(lat, long) { + return new global.google.maps.LatLng(lat, long); +}; + +var getSearchType = exports.getSearchType = function getSearchType(_ref) { + var _ref$type = _ref.type, + type = _ref$type === undefined ? 'locality' : _ref$type; + return _constants.validSearchTypes[type] || _constants.validSearchTypes.geocode; +}; + +var emptyResults = exports.emptyResults = function emptyResults(s) { + return { status: s }; +}; + +var findPlaceByType = exports.findPlaceByType = function findPlaceByType(data, placeType) { + var placeTypes = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + return data.find(function (d) { + return placeTypes[d.types[0]] === placeType; + }) || {}; +}; + +var getPlaceTypes = exports.getPlaceTypes = function getPlaceTypes(_ref2) { + var _ref2$outputPlaceType = _ref2.outputPlaceTypes, + outputPlaceTypes = _ref2$outputPlaceType === undefined ? [] : _ref2$outputPlaceType; + + return Object.keys(_constants.defaultPlaceTypes).reduce(function (types, type) { + if (!outputPlaceTypes.includes(_constants.defaultPlaceTypes[type])) { + return types; + } + + return Object.assign({}, types, _defineProperty({}, type, _constants.defaultPlaceTypes[type])); + }, {}); +}; + +var getRestrictions = exports.getRestrictions = function getRestrictions(_ref3) { + var _ref3$filterByCountry = _ref3.filterByCountry, + country = _ref3$filterByCountry === undefined ? '' : _ref3$filterByCountry; + return { country: country }; +}; + +var getSearchStrategies = exports.getSearchStrategies = function getSearchStrategies(_ref4) { + var searchStrategies = _ref4.searchStrategies; + + if (!searchStrategies) { + return _constants.validStrategies; + } + + if (!Array.isArray(searchStrategies)) { + throw new Error('searchStrategies should be of type array'); + } + + return searchStrategies.filter(function (s) { + var isValid = _constants.validStrategies.includes(s); + + if (!isValid) { + console.error(s + ' is not a valid strategy - skipped'); + + return false; + } + + return true; + }); +}; + +var pipeStrategies = exports.pipeStrategies = function pipeStrategies(selectedStrategies, allStrategies, resolve) { + var _selectedStrategies = _toArray(selectedStrategies), + firstFunc = _selectedStrategies[0], + restFuncs = _selectedStrategies.slice(1); + + if (selectedStrategies.some(function (s) { + return typeof allStrategies[s] !== 'function'; + })) { + return {}; + } + + if (!restFuncs.length) { + return resolve(allStrategies[firstFunc]()); + } + + return restFuncs.reduce(function (pre, func) { + return resolve(pre, allStrategies[func]); + }, allStrategies[firstFunc]()); +}; + +var rejectByRegex = exports.rejectByRegex = function rejectByRegex(predictions, regex) { + if (!regex) { + return predictions; + } + + return predictions.filter(function (p) { + return !regex.test(p.description); + }); +}; + +exports.default = { + getLatLong: getLatLong, + getSearchType: getSearchType, + getRestrictions: getRestrictions, + emptyResults: emptyResults, + findPlaceByType: findPlaceByType, + getPlaceTypes: getPlaceTypes, + getSearchStrategies: getSearchStrategies, + pipeStrategies: pipeStrategies, + rejectByRegex: rejectByRegex +}; \ No newline at end of file diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 0000000..344013d --- /dev/null +++ b/dist/index.js @@ -0,0 +1,254 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; + +exports.default = googlePlaces; + +var _config = require('./config'); + +var _config2 = _interopRequireDefault(_config); + +var _constants = require('./constants'); + +var _helpers = require('./helpers'); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } + +function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; } + +function googlePlaces(_ref) { + var options = _objectWithoutProperties(_ref, []); + + var _initialize = (0, _config2.default)(), + getGoogleAutocompleteService = _initialize.getGoogleAutocompleteService, + getGooglePlacesService = _initialize.getGooglePlacesService, + getGeocoder = _initialize.getGeocoder; + + var googleAutocompleteService = getGoogleAutocompleteService(); + var googlePlacesService = getGooglePlacesService(); + var geocoder = getGeocoder(); + + this.longitude = null; + this.latitude = null; + + var componentRestrictions = (0, _helpers.getRestrictions)(options); + var placeTypes = (0, _helpers.getPlaceTypes)(options); + var filterType = (0, _helpers.getSearchType)(options); + var searchStrategies = (0, _helpers.getSearchStrategies)(options); + + return { + getPredictions: function getPredictions() { + var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; + var callback = arguments[1]; + var rejectRegex = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null; + + return new Promise(function (resolve, reject) { + if (!input) { + reject(_constants.status.INVALID_INPUT); + return; + } + + googleAutocompleteService.getPlacePredictions({ + input: input, + componentRestrictions: componentRestrictions, + types: [filterType] + }, function (predictions, responseStatus) { + if (responseStatus !== _constants.googleStatus.OK) { + resolve(_constants.status.NO_RESULTS); + callback({}); + return; + } + + resolve(_constants.status.SUCCESS); + callback((0, _helpers.rejectByRegex)(predictions, rejectRegex).reduceRight(function (results, p) { + return Object.assign(_defineProperty({}, p.place_id, { + body: p.description, + type: p.types[0], + terms: p.terms.map(function (t) { + return t.value; + }) + }), results); + }, {})); + }); + }); + }, + getPlace: function getPlace(placeId, prediction, callback) { + var _this = this; + + if (!prediction || (typeof prediction === 'undefined' ? 'undefined' : _typeof(prediction)) !== 'object') { + prediction = {}; + } + + var _prediction = prediction, + predictionType = _prediction.type, + _prediction$terms = _prediction.terms, + terms = _prediction$terms === undefined ? [] : _prediction$terms, + body = _prediction.body; + + var predictionTerms = terms.slice(); + + if (placeId === '' || !body) { + return callback((0, _helpers.emptyResults)(_constants.status.NO_RESULTS)); + } + + var resultComponents = {}; + + var getTermIndex = function getTermIndex(longName, shortName) { + var index = predictionTerms.indexOf(longName); + + return index > -1 ? index : predictionTerms.indexOf(shortName); + }; + + var getAddressComponents = function getAddressComponents() { + var places = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; + + var placeComponents = places.reduce(function (components, c) { + var placeType = c.types.find(function (t) { + return _constants.defaultPlaceTypes[t]; + }); + var type = placeTypes[placeType]; + + var termIndex = getTermIndex(c.long_name, c.short_name); + if (termIndex === -1 && !type) { + return components; + } + + var term = void 0; + if (termIndex > -1 && type) { + term = predictionTerms.splice(termIndex, 1); + } + + if (!placeType || !term && resultComponents[type] || type === placeTypes.postal_code && placeTypes[predictionType] === placeTypes.locality) { + return components; + } + + if (type === placeTypes.administrative_area_level_1) { + return Object.assign({}, components, { + administrativeAreaLevel1Code: c.short_name, + administrativeAreaLevel1: c.long_name + }); + } + + return Object.assign({}, components, _defineProperty({}, _constants.defaultPlaceTypes[placeType], c.long_name)); + }, {}); + + resultComponents = Object.assign({}, resultComponents, placeComponents); + + return resultComponents; + }; + + var searchByPlaceId = function searchByPlaceId() { + var addressComponents = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + return new Promise(function (resolve, reject) { + googlePlacesService.getDetails({ + placeId: placeId + }, function (place, responseStatus) { + + if (responseStatus !== _constants.googleStatus.OK) { + reject(responseStatus); + return; + } + + _this.longitude = place.geometry.location.lng(); + _this.latitude = place.geometry.location.lat(); + + resolve(Object.assign({}, addressComponents, getAddressComponents(place.address_components))); + }); + }); + }; + + var searchByText = function searchByText() { + var addressComponents = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + return new Promise(function (resolve, reject) { + googlePlacesService.textSearch({ + query: body + }, function (places, responseStatus) { + + if (responseStatus !== _constants.googleStatus.OK) { + reject(responseStatus); + return; + } + + var place = places[0]; + + _this.longitude = place.geometry.location.lng(); + _this.latitude = place.geometry.location.lat(); + + resolve(Object.assign({}, addressComponents, getAddressComponents(place.address_components))); + }); + }); + }; + + var searchWithGeocoder = function searchWithGeocoder() { + var addressComponents = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + return new Promise(function (resolve, reject) { + geocoder.geocode({ + latLng: (0, _helpers.getLatLong)(_this.latitude, _this.longitude) + }, function (results, responseStatus) { + if (responseStatus !== _constants.googleStatus.OK) { + reject(responseStatus); + return; + } + + if (placeTypes[predictionType]) { + [placeTypes.administrative_area_level_1, placeTypes.locality, placeTypes.postal_code, placeTypes.street_address].some(function (type) { + var place = (0, _helpers.findPlaceByType)(results, type, placeTypes); + + addressComponents = Object.assign({}, addressComponents, getAddressComponents(place.address_components)); + + return type === placeTypes[predictionType]; + }); + } + + resolve(addressComponents); + return; + }); + }); + }; + + var outputResult = function outputResult(addressComponents) { + return callback(Object.assign({ + coords: _this.latitude + ', ' + _this.longitude + }, addressComponents, { + notValid: predictionTerms, + status: predictionTerms.length ? _constants.status.PARTIAL_SUCCESS : _constants.status.SUCCESS + })); + }; + + var resolveFunc = function resolveFunc(func, nextFunc) { + return func.then(function (addressComponents) { + var hasEmptyValues = (_constants.outputTypes[predictionType] || []).some(function (t) { + return !addressComponents[t]; + }); + + return nextFunc && (predictionTerms.length || hasEmptyValues) ? nextFunc(addressComponents) : addressComponents; + }); + }; + + try { + var placeStrategies = { + searchByPlaceId: searchByPlaceId, + searchByText: searchByText, + searchWithGeocoder: searchWithGeocoder + }; + + return (0, _helpers.pipeStrategies)(searchStrategies, placeStrategies, resolveFunc).then(function (addressComponents) { + return outputResult(addressComponents); + }).catch(function (error) { + return callback((0, _helpers.emptyResults)(error)); + }); + } catch (e) { + return callback((0, _helpers.emptyResults)(_constants.status.NO_RESULTS)); + } + } + }; +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..823464e --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "google_places", + "version": "0.0.0", + "description": "A simple factory using google places API to fetch predictions for places and get place details.", + "main": "dist/index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "prebuild": "rimraf dist", + "build": "babel --out-dir dist src" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/novykh/google-places-autocomplete-service.git" + }, + "keywords": [ + "google", + "places", + "autocomplete" + ], + "author": "Johnny Klironomos ", + "license": "MIT", + "bugs": { + "url": "https://github.com/novykh/google-places-autocomplete-service/issues" + }, + "homepage": "https://github.com/novykh/google-places-autocomplete-service#readme", + "devDependencies": { + "babel-cli": "^6.23.0", + "babel-plugin-transform-object-rest-spread": "^6.23.0", + "babel-preset-es2015": "^6.22.0", + "rimraf": "^2.6.1" + } +} diff --git a/src/config.js b/src/config.js new file mode 100644 index 0000000..dbc63ba --- /dev/null +++ b/src/config.js @@ -0,0 +1,21 @@ +export default function init() { + if (!global.google || !global.google.maps) { + throw new Error('Please import google maps'); + } + + if (!(/3\.\d\d\.\d+/).test(global.google.maps.version)) { + throw new Error('Please import a google maps version 3+'); + } + + return { + getGoogleAutocompleteService: () => { + return new global.google.maps.places + .AutocompleteService(); + }, + getGooglePlacesService: () => { + return new global.google.maps.places + .PlacesService(document.createElement('div')); + }, + getGeocoder: () => new global.google.maps.Geocoder() + }; +} diff --git a/src/constants.js b/src/constants.js new file mode 100644 index 0000000..2c0f7bf --- /dev/null +++ b/src/constants.js @@ -0,0 +1,96 @@ +export const googleStatus = {}; +({ + ERROR: googleStatus.ERROR, + // There was a problem contacting the Google servers. + INVALID_REQUEST: googleStatus.INVALID_REQUEST, + // This GeocoderRequest was invalid. + OK: googleStatus.OK, + // The response contains a valid GeocoderResponse. + OVER_QUERY_LIMIT: googleStatus.OVER_QUERY_LIMIT, + // The webpage has gone over the requests limit in too short a period of time. + REQUEST_DENIED: googleStatus.REQUEST_DENIED, + // The webpage is not allowed to use the geocoder. + UNKNOWN_ERROR: googleStatus.UNKNOWN_ERROR, + // A geocoding request could not be processed due to a server error. The request may succeed if you try again. + ZERO_RESULTS: googleStatus.ZERO_RESULTS + // No result was found for this GeocoderRequest. +} = google.maps.places.PlacesServiceStatus); + +export const status = { + INVALID_INPUT: 'INVALID_INPUT', + NO_RESULTS: 'NO_RESULTS', + SUCCESS: 'SUCCESS', + PARTIAL_SUCCESS: 'PARTIAL_SUCCESS' +}; + +export const defaultPlaceTypes = { + street_address: 'streetAddress', + route: 'route', + street_number: 'streetNumber', + neighborhood: 'neighborhood', + postal_code: 'postalCode', + sublocality: 'sublocality', + locality: 'locality', + administrative_area_level_1: 'administrativeAreaLevel1', + administrative_area_level_2: 'administrativeAreaLevel2', + administrative_area_level_3: 'administrativeAreaLevel3', + administrative_area_level_4: 'administrativeAreaLevel4', + administrative_area_level_5: 'administrativeAreaLevel5', + country: 'country' +}; + +export const validSearchTypes = { + geocode: 'geocode', + address: 'address', + establishment: 'establishment', + '(regions)': '(regions)', + '(cities)': '(cities)' +}; + +export const validStrategies = [ + 'searchByPlaceId', + 'searchByText', + 'searchWithGeocoder' +]; + +export const outputTypes = { + street_address: [ + defaultPlaceTypes.streetAddress, + defaultPlaceTypes.route, + defaultPlaceTypes.streetNumber, + defaultPlaceTypes.neighborhood, + defaultPlaceTypes.postalCode, + defaultPlaceTypes.sublocality, + defaultPlaceTypes.locality, + defaultPlaceTypes.administrativeAreaLevel1, + defaultPlaceTypes.administrativeAreaLevel2, + defaultPlaceTypes.administrativeAreaLevel3, + defaultPlaceTypes.administrativeAreaLevel4, + defaultPlaceTypes.administrativeAreaLevel5, + defaultPlaceTypes.country + ], + postal_code: [ + defaultPlaceTypes.postalCode, + defaultPlaceTypes.sublocality, + defaultPlaceTypes.locality, + defaultPlaceTypes.administrativeAreaLevel1, + defaultPlaceTypes.administrativeAreaLevel2, + defaultPlaceTypes.administrativeAreaLevel3, + defaultPlaceTypes.administrativeAreaLevel4, + defaultPlaceTypes.administrativeAreaLevel5, + defaultPlaceTypes.country + ], + locality: [ + defaultPlaceTypes.locality, + defaultPlaceTypes.administrativeAreaLevel1, + defaultPlaceTypes.administrativeAreaLevel2, + defaultPlaceTypes.administrativeAreaLevel3, + defaultPlaceTypes.administrativeAreaLevel4, + defaultPlaceTypes.administrativeAreaLevel5, + defaultPlaceTypes.country + ], + administrative_area_level_1: [ + defaultPlaceTypes.administrativeAreaLevel1, + defaultPlaceTypes.country + ] +}; diff --git a/src/helpers.js b/src/helpers.js new file mode 100644 index 0000000..27e050d --- /dev/null +++ b/src/helpers.js @@ -0,0 +1,89 @@ +import { + defaultPlaceTypes, + validSearchTypes, + validStrategies +} from './constants'; + +export const getLatLong = (lat, long) => new global.google.maps.LatLng(lat, long); + +export const getSearchType = ({type = 'locality'}) => validSearchTypes[type] || validSearchTypes.geocode; + +export const emptyResults = s => ({status: s}); + +export const findPlaceByType = (data, placeType, placeTypes = {}) => data.find(d => { + return placeTypes[d.types[0]] === placeType; +}) || {}; + +export const getPlaceTypes = ({outputPlaceTypes = []}) => { + return Object.keys(defaultPlaceTypes).reduce((types, type) => { + if (!outputPlaceTypes.includes(defaultPlaceTypes[type])) { + return types; + } + + return { + ...types, + [type]: defaultPlaceTypes[type] + }; + }, {}); +}; + +export const getRestrictions = ({filterByCountry: country = ''}) => ({country}); + +export const getSearchStrategies = ({searchStrategies}) => { + if (!searchStrategies) { + return validStrategies; + } + + if (!Array.isArray(searchStrategies)) { + throw new Error('searchStrategies should be of type array'); + } + + return searchStrategies.filter(s => { + const isValid = validStrategies.includes(s); + + if (!isValid) { + console.error(`${s} is not a valid strategy - skipped`); + + return false; + } + + return true; + }); +}; + +export const pipeStrategies = (selectedStrategies, allStrategies, resolve) => { + const [firstFunc, ...restFuncs] = selectedStrategies; + + if (selectedStrategies.some(s => typeof allStrategies[s] !== 'function')) { + return {}; + } + + if (!restFuncs.length) { + return resolve(allStrategies[firstFunc]()); + } + + return restFuncs.reduce((pre, func) => { + return resolve(pre, allStrategies[func]); + }, allStrategies[firstFunc]()); +}; + +export const rejectByRegex = (predictions, regex) => { + if (!regex) { + return predictions; + } + + return predictions + .filter(p => !regex.test(p.description)); +}; + +export default { + getLatLong, + getSearchType, + getRestrictions, + emptyResults, + findPlaceByType, + getPlaceTypes, + getSearchStrategies, + pipeStrategies, + rejectByRegex +}; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..8217b8e --- /dev/null +++ b/src/index.js @@ -0,0 +1,263 @@ +import initialize from './config'; +import { + status, + googleStatus, + defaultPlaceTypes, + outputTypes +} from './constants'; +import { + getLatLong, + getSearchType, + getPlaceTypes, + getSearchStrategies, + getRestrictions, + emptyResults, + rejectByRegex, + findPlaceByType, + pipeStrategies +} from './helpers'; + +export default function googlePlaces({...options}) { + const { + getGoogleAutocompleteService, + getGooglePlacesService, + getGeocoder + } = initialize(); + + const googleAutocompleteService = getGoogleAutocompleteService(); + const googlePlacesService = getGooglePlacesService(); + const geocoder = getGeocoder(); + + this.longitude = null; + this.latitude = null; + + const componentRestrictions = getRestrictions(options); + const placeTypes = getPlaceTypes(options); + const filterType = getSearchType(options); + const searchStrategies = getSearchStrategies(options); + + return { + getPredictions(input = '', callback, rejectRegex = null) { + return new Promise((resolve, reject) => { + if (!input) { + reject(status.INVALID_INPUT); + return; + } + + googleAutocompleteService.getPlacePredictions({ + input, + componentRestrictions, + types: [filterType] + }, (predictions, responseStatus) => { + if (responseStatus !== googleStatus.OK) { + resolve(status.NO_RESULTS); + callback({}); + return; + } + + resolve(status.SUCCESS); + callback( + rejectByRegex(predictions, rejectRegex) + .reduceRight((results, p) => { + return { + [p.place_id]: { + body: p.description, + type: p.types[0], + terms: p.terms.map(t => t.value) + }, + ...results + }; + }, {}) + ); + }); + }); + }, + + getPlace(placeId, prediction, callback) { + if (!prediction || typeof prediction !== 'object') { + prediction = {}; + } + + const { + type: predictionType, + terms = [], + body + } = prediction; + const predictionTerms = terms.slice(); + + if (placeId === '' || !body) { + return callback(emptyResults(status.NO_RESULTS)); + } + + let resultComponents = {}; + + const getTermIndex = (longName, shortName) => { + const index = predictionTerms.indexOf(longName); + + return index > -1 ? index : predictionTerms.indexOf(shortName); + }; + + const getAddressComponents = (places = []) => { + const placeComponents = places.reduce((components, c) => { + const placeType = c.types + .find(t => defaultPlaceTypes[t]); + const type = placeTypes[placeType]; + + const termIndex = getTermIndex(c.long_name, c.short_name); + if (termIndex === -1 && !type) { + return components; + } + + let term; + if (termIndex > -1 && type) { + term = predictionTerms.splice(termIndex, 1); + } + + if ( + !placeType + || (!term && resultComponents[type]) + || (type === placeTypes.postal_code + && placeTypes[predictionType] === placeTypes.locality) + ) { + return components; + } + + if (type === placeTypes.administrative_area_level_1) { + return { + ...components, + administrativeAreaLevel1Code: c.short_name, + administrativeAreaLevel1: c.long_name + }; + } + + return { + ...components, + [defaultPlaceTypes[placeType]]: c.long_name + }; + }, {}); + + resultComponents = { + ...resultComponents, + ...placeComponents + }; + + return resultComponents; + }; + + const searchByPlaceId = (addressComponents = {}) => { + return new Promise((resolve, reject) => { + googlePlacesService.getDetails({ + placeId + }, (place, responseStatus) => { + + if (responseStatus !== googleStatus.OK) { + reject(responseStatus); + return; + } + + this.longitude = place.geometry.location.lng(); + this.latitude = place.geometry.location.lat(); + + resolve({ + ...addressComponents, + ...getAddressComponents(place.address_components) + }); + }); + }); + }; + + const searchByText = (addressComponents = {}) => { + return new Promise((resolve, reject) => { + googlePlacesService.textSearch({ + query: body + }, (places, responseStatus) => { + + if (responseStatus !== googleStatus.OK) { + reject(responseStatus); + return; + } + + const place = places[0]; + + this.longitude = place.geometry.location.lng(); + this.latitude = place.geometry.location.lat(); + + resolve({ + ...addressComponents, + ...getAddressComponents(place.address_components) + }); + }); + }); + }; + + const searchWithGeocoder = (addressComponents = {}) => { + return new Promise((resolve, reject) => { + geocoder.geocode({ + latLng: getLatLong(this.latitude, this.longitude) + }, (results, responseStatus) => { + if (responseStatus !== googleStatus.OK) { + reject(responseStatus); + return; + } + + if (placeTypes[predictionType]) { + [ + placeTypes.administrative_area_level_1, + placeTypes.locality, + placeTypes.postal_code, + placeTypes.street_address + ].some(type => { + const place = findPlaceByType(results, type, placeTypes); + + addressComponents = { + ...addressComponents, + ...getAddressComponents(place.address_components) + }; + + return type === placeTypes[predictionType]; + }); + } + + resolve(addressComponents); + return; + }); + }); + }; + + const outputResult = addressComponents => callback({ + coords: `${this.latitude}, ${this.longitude}`, + ...addressComponents, + notValid: predictionTerms, + status: predictionTerms.length + ? status.PARTIAL_SUCCESS + : status.SUCCESS + }); + + const resolveFunc = (func, nextFunc) => { + return func.then(addressComponents => { + const hasEmptyValues = (outputTypes[predictionType] || []) + .some(t => !addressComponents[t]); + + return nextFunc && (predictionTerms.length || hasEmptyValues) + ? nextFunc(addressComponents) + : addressComponents; + }); + }; + + try { + const placeStrategies = { + searchByPlaceId, + searchByText, + searchWithGeocoder + }; + + return pipeStrategies(searchStrategies, placeStrategies, resolveFunc) + .then(addressComponents => outputResult(addressComponents)) + .catch(error => callback(emptyResults(error))); + + } catch (e) { + return callback(emptyResults(status.NO_RESULTS)); + } + } + }; +}