From 3a9d400f05ce87f0c86ac1eac6a3a78ca5bbe2b2 Mon Sep 17 00:00:00 2001 From: bartronx7 Date: Sun, 8 Jul 2018 10:59:43 -0500 Subject: [PATCH] get places from google by location and search --- js/apis/GooglePlacesApi.js | 79 ++++++++++++++++++++++++++++++++++++++ js/apis/PlaceDetailsApi.js | 39 ------------------- js/pages/PlacesList.js | 4 +- js/records/PlaceRecord.js | 15 ++++++-- js/streams/PlacesStream.js | 45 ++++++++++++---------- 5 files changed, 118 insertions(+), 64 deletions(-) create mode 100644 js/apis/GooglePlacesApi.js delete mode 100644 js/apis/PlaceDetailsApi.js diff --git a/js/apis/GooglePlacesApi.js b/js/apis/GooglePlacesApi.js new file mode 100644 index 0000000..2f6977d --- /dev/null +++ b/js/apis/GooglePlacesApi.js @@ -0,0 +1,79 @@ +// @flow +import { type GooglePlaceObj } from '../records/PlaceRecord'; +import { GoogleAPIKey } from '../constants/AppConstants'; +import { pipe, filter, path, contains } from 'ramda'; + +const placesDetailUrl = `https://maps.googleapis.com/maps/api/place/details/json?key=${GoogleAPIKey}`; +const photosUrl = `https://maps.googleapis.com/maps/api/place/photo?key=${GoogleAPIKey}`; +const placesFromTextUrl = `https://maps.googleapis.com/maps/api/place/findplacefromtext/json?key=${GoogleAPIKey}`; + +type GooglePlaceDetailsResponse = { error_message: ?string, result: GooglePlaceObj }; + +const milesToMeters = miles => Math.ceil(miles > 0 ? miles / 0.00062137 : 0); + +const isEstablishment = pipe( + path(['types']), + contains('establishment') +); + +export const getPlaceDetails = async (placeid: ?string): Promise => { + if (!placeid || typeof placeid !== 'string') { + throw new Error('placeid looks wrong'); + } + + const res = await fetch(`${placesDetailUrl}&placeid=${placeid}`); + + const { error_message, result }: GooglePlaceDetailsResponse = await res.json(); + + if (error_message) { + throw new Error(error_message); + } + return result; +}; + +export const getURLForPhotoReference = (opts?: { maxheight?: number, maxwidth?: number } = {}) => ( + photoReference: string +) => { + if (!photoReference) { + return ''; + } + + const { maxheight, maxwidth } = opts; + + const maxHeight = `&maxheight=${maxheight || 600}`; + const maxWidth = maxwidth ? `&maxwidth=${maxwidth}` : ''; + const photoref = `&photoreference=${photoReference}`; + + return `${photosUrl}${photoref}${maxHeight}${maxWidth}`; +}; + +export const findNearbyPlaces = async ({ + location, + search, + radius, +}: { + location?: Position, + search?: string, + radius: number, +}): Promise => { + if (!location) { + return null; + } + + const { + coords: { latitude, longitude }, + } = location; + + const input = `input=${encodeURIComponent(search || '*')}&inputtype=textquery`; + const loc = `locationbias=circle:${milesToMeters(radius)}@${latitude},${longitude}`; + const fields = 'fields=id,name,geometry,photos,types'; + const reqUrl = `${placesFromTextUrl}&${input}&${loc}&${fields}`; + + const { candidates, status } = await (await fetch(reqUrl)).json(); + + if (status !== 'OK') { + throw new Error('google find places request failed'); + } + + return filter(isEstablishment, candidates); +}; diff --git a/js/apis/PlaceDetailsApi.js b/js/apis/PlaceDetailsApi.js deleted file mode 100644 index 8d849c4..0000000 --- a/js/apis/PlaceDetailsApi.js +++ /dev/null @@ -1,39 +0,0 @@ -// @flow -import { type GooglePlaceObj } from '../records/PlaceRecord'; -import { GoogleAPIKey } from '../constants/AppConstants'; - -const placesUrl = `https://maps.googleapis.com/maps/api/place/details/json?key=${GoogleAPIKey}`; -const photosUrl = `https://maps.googleapis.com/maps/api/place/photo?key=${GoogleAPIKey}`; - -type GooglePlaceDetailsResponse = { error_message: ?string, result: GooglePlaceObj }; - -export const getPlaceDetails = async (placeid: ?string): Promise => { - if (!placeid || typeof placeid !== 'string') { - throw new Error('placeid looks wrong'); - } - - const res = await fetch(`${placesUrl}&placeid=${placeid}`); - - const { error_message, result }: GooglePlaceDetailsResponse = await res.json(); - - if (error_message) { - throw new Error(error_message); - } - return result; -}; - -export const getURLForPhotoReference = (opts?: { maxheight?: number, maxwidth?: number } = {}) => ( - photoReference: string -) => { - if (!photoReference) { - return ''; - } - - const { maxheight, maxwidth } = opts; - - const maxHeight = `&maxheight=${maxheight || 600}`; - const maxWidth = maxwidth ? `&maxwidth=${maxwidth}` : ''; - const photoref = `&photoreference=${photoReference}`; - - return `${photosUrl}${photoref}${maxHeight}${maxWidth}`; -}; diff --git a/js/pages/PlacesList.js b/js/pages/PlacesList.js index cf753fd..a233f09 100644 --- a/js/pages/PlacesList.js +++ b/js/pages/PlacesList.js @@ -5,7 +5,7 @@ import PlaceTile from '../components/PlaceTile'; import { compose, pure, withState, withHandlers } from 'recompose'; import { withFoodItemsGroupedByPlace } from '../enhancers/foodItemEnhancers'; import { withPlaces } from '../enhancers/placeEnhancers'; -import { type Map, List } from 'immutable'; +import { Map, List } from 'immutable'; import typeof FoodItemRecord from '../records/FoodItemRecord'; import typeof PlaceRecord from '../records/PlaceRecord'; @@ -17,7 +17,7 @@ type Props = { isRefreshing: boolean, }; -const PlacesList = ({ foodItemsByPlace, places, isRefreshing, onPulldown }: Props) => { +const PlacesList = ({ foodItemsByPlace, places = Map(), isRefreshing, onPulldown }: Props) => { return ( }> diff --git a/js/records/PlaceRecord.js b/js/records/PlaceRecord.js index 9ee47db..0310829 100644 --- a/js/records/PlaceRecord.js +++ b/js/records/PlaceRecord.js @@ -1,7 +1,7 @@ // @flow import { List, Map, Record, fromJS } from 'immutable'; import { pipe, pathOr, map, head, memoizeWith } from 'ramda'; -import { getURLForPhotoReference } from '../apis/PlaceDetailsApi'; +import { getURLForPhotoReference } from '../apis/GooglePlacesApi'; export type GooglePlaceObj = { place_id: string, @@ -80,9 +80,18 @@ const getPhotos = pathOr([{}], ['photos']); const getPhotoRef = photo => photo.photo_reference || ''; -const getThumb = pipe(getPhotos, head, getPhotoRef, getURLForPhotoReference({ maxheight: 200 })); +const getThumb = pipe( + getPhotos, + head, + getPhotoRef, + getURLForPhotoReference({ maxheight: 200 }) +); -const getPhotoUrls = pipe(getPhotos, map(getPhotoRef), map(getURLForPhotoReference({ maxheight: 600 }))); +const getPhotoUrls = pipe( + getPhotos, + map(getPhotoRef), + map(getURLForPhotoReference({ maxheight: 600 })) +); export const buildPlaceRecord = memoizeWith( (place: ?GooglePlaceObj) => place && place.place_id, diff --git a/js/streams/PlacesStream.js b/js/streams/PlacesStream.js index 6c168c7..4d3c789 100644 --- a/js/streams/PlacesStream.js +++ b/js/streams/PlacesStream.js @@ -1,33 +1,38 @@ // @flow import { buildPlaceRecord } from '../records/PlaceRecord'; import { Map } from 'immutable'; -import foodItems$ from './FoodItemsStream'; -import { getPlaceDetails } from '../apis/PlaceDetailsApi'; +import { findNearbyPlaces } from '../apis/GooglePlacesApi'; import { memoize } from 'ramda'; -import { Observable } from 'rxjs'; -import typeof FoodItemRecord from '../records/FoodItemRecord'; import { type GooglePlaceObj } from '../records/PlaceRecord'; import { setById } from '../helpers/ImmutableHelpers'; +import location$ from './LocationStream'; +import filter$ from './FilterStream'; +import FilterRecord from '../records/FilterRecord'; /** - * return a promise of a place details object - * if already requested return existing promise - * swallow exceptions so as not to break the stream + * always return a promise and swallow exceptions so as not to break the stream */ -const safeGetPlaceDetails = memoize((placeId: string): Promise => { - return getPlaceDetails(placeId).catch(error => { - console.log('ERROR getPlaceDetails: ', error); // eslint-disable-line no-console - return null; +const safeWrapAjax = ajaxFn => + memoize((...args) => { + return ajaxFn(...args).catch(error => { + console.log('ERROR: ', error); //eslint-disable-line no-console + return null; + }); }); -}); -const Places$ = foodItems$ - .mergeMap((foodItems: Map = Map()): Observable => { - return Observable.from(foodItems.toArray().map(foodItem => foodItem.placeId)); - }) - .distinct() - .mergeMap(safeGetPlaceDetails) - .map(buildPlaceRecord) - .scan(setById, new Map()); +const safeFindNearbyPlaces = safeWrapAjax(findNearbyPlaces); + +const Places$ = location$ + .combineLatest(filter$) + .mergeMap(([location, filter]: [?Position, FilterRecord]) => + safeFindNearbyPlaces({ + location, + radius: filter.radius, + search: filter.search, + }) + ) + .map((places: Array) => { + return (places || []).map(buildPlaceRecord).reduce(setById, new Map()); + }); export default Places$;