Skip to content

Commit

Permalink
feat: improve Places components' compatibility with GA versions of th…
Browse files Browse the repository at this point in the history
…e Maps JS SDK

PiperOrigin-RevId: 555285127
  • Loading branch information
awmack authored and copybara-github committed Aug 9, 2023
1 parent ba61ae5 commit 08a269b
Show file tree
Hide file tree
Showing 10 changed files with 257 additions and 10 deletions.
6 changes: 6 additions & 0 deletions src/api_loader/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ for Extended Components.
To use this component, make sure you [sign up for Google Maps Platform and
create an API
key](https://console.cloud.google.com/google/maps-apis/start?utm_source=github&utm_medium=documentation&utm_campaign=&utm_content=web_components).
By default, the API loader component will request the beta version of the
Maps JavaScript API, giving you access to additional components <a
href="https://developers.google.com/maps/documentation/javascript/web-components/overview?utm_source=github&utm_medium=documentation&utm_campaign=&utm_content=web_components">`<gmp-map>`
and `<gmp-advanced-marker>`</a>. However, you can set the `version` attribute
to select a stable (General Availability) version of the SDK such as
`weekly`.

## Importing

Expand Down
6 changes: 6 additions & 0 deletions src/api_loader/api_loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ declare global {
* To use this component, make sure you [sign up for Google Maps Platform and
* create an API
* key](https://console.cloud.google.com/google/maps-apis/start?utm_source=github&utm_medium=documentation&utm_campaign=&utm_content=web_components).
* By default, the API loader component will request the beta version of the
* Maps JavaScript API, giving you access to additional components <a
* href="https://developers.google.com/maps/documentation/javascript/web-components/overview?utm_source=github&utm_medium=documentation&utm_campaign=&utm_content=web_components">`<gmp-map>`
* and `<gmp-advanced-marker>`</a>. However, you can set the `version` attribute
* to select a stable (General Availability) version of the SDK such as
* `weekly`.
*/
@customElement('gmpx-api-loader')
export class APILoader extends BaseComponent {
Expand Down
5 changes: 4 additions & 1 deletion src/place_building_blocks/place_data_provider/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,10 @@ Used when fetching Places data for the place specified via attribute/property.

#### Documentation

Places API [Place class documentation](https://developers.google.com/maps/documentation/javascript/place?utm_source=github&utm_medium=documentation&utm_campaign=&utm_content=web_components). Please be sure to check this documentation for additional requirements and recommendations regarding your use.
* When using the Maps JavaScript API beta version (default): [Place class documentation](https://developers.google.com/maps/documentation/javascript/place?utm_source=github&utm_medium=documentation&utm_campaign=&utm_content=web_components)
* When using the Maps JavaScript API GA version: [Place details documentation](https://developers.google.com/maps/documentation/javascript/examples/place-details?utm_source=github&utm_medium=documentation&utm_campaign=&utm_content=web_components)

Please be sure to check this documentation for additional requirements and recommendations regarding your use.

#### Pricing

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ Used when fetching Places data for the place specified via attribute/property.

#### Documentation

Places API [Place class documentation](https://developers.google.com/maps/documentation/javascript/place?utm_source=github&utm_medium=documentation&utm_campaign=&utm_content=web_components). Please be sure to check this documentation for additional requirements and recommendations regarding your use.
* When using the Maps JavaScript API beta version (default): [Place class documentation](https://developers.google.com/maps/documentation/javascript/place?utm_source=github&utm_medium=documentation&utm_campaign=&utm_content=web_components)
* When using the Maps JavaScript API GA version: [Place details documentation](https://developers.google.com/maps/documentation/javascript/examples/place-details?utm_source=github&utm_medium=documentation&utm_campaign=&utm_content=web_components)

Please be sure to check this documentation for additional requirements and recommendations regarding your use.

#### Pricing

Expand Down
7 changes: 6 additions & 1 deletion src/place_overview/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,12 @@ Used when fetching Places data for the place specified via attribute/property.

#### Documentation

Places API [Place class documentation](https://developers.google.com/maps/documentation/javascript/place?utm_source=github&utm_medium=documentation&utm_campaign=&utm_content=web_components) and Places API [Photos documentation](https://developers.google.com/maps/documentation/javascript/places?utm_source=github&utm_medium=documentation&utm_campaign=&utm_content=web_components#places_photos) (photos used in medium, large, and x-large sizes). Please be sure to check this documentation for additional requirements and recommendations regarding your use.
* Fetching Places data:
* Maps JavaScript API beta version (default): [Place class documentation](https://developers.google.com/maps/documentation/javascript/place?utm_source=github&utm_medium=documentation&utm_campaign=&utm_content=web_components)
* Maps JavaScript API GA version: [Place details documentation](https://developers.google.com/maps/documentation/javascript/examples/place-details?utm_source=github&utm_medium=documentation&utm_campaign=&utm_content=web_components)
* Displaying Places photos (used in medium, large, and x-large sizes): [Photos documentation](https://developers.google.com/maps/documentation/javascript/places?utm_source=github&utm_medium=documentation&utm_campaign=&utm_content=web_components#places_photos)

Please be sure to check this documentation for additional requirements and recommendations regarding your use.

#### Pricing

Expand Down
7 changes: 6 additions & 1 deletion src/place_overview/doc_src/README.apis.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ Used when fetching Places data for the place specified via attribute/property.

#### Documentation

Places API [Place class documentation](https://developers.google.com/maps/documentation/javascript/place?utm_source=github&utm_medium=documentation&utm_campaign=&utm_content=web_components) and Places API [Photos documentation](https://developers.google.com/maps/documentation/javascript/places?utm_source=github&utm_medium=documentation&utm_campaign=&utm_content=web_components#places_photos) (photos used in medium, large, and x-large sizes). Please be sure to check this documentation for additional requirements and recommendations regarding your use.
* Fetching Places data:
* Maps JavaScript API beta version (default): [Place class documentation](https://developers.google.com/maps/documentation/javascript/place?utm_source=github&utm_medium=documentation&utm_campaign=&utm_content=web_components)
* Maps JavaScript API GA version: [Place details documentation](https://developers.google.com/maps/documentation/javascript/examples/place-details?utm_source=github&utm_medium=documentation&utm_campaign=&utm_content=web_components)
* Displaying Places photos (used in medium, large, and x-large sizes): [Photos documentation](https://developers.google.com/maps/documentation/javascript/places?utm_source=github&utm_medium=documentation&utm_campaign=&utm_content=web_components#places_photos)

Please be sure to check this documentation for additional requirements and recommendations regarding your use.

#### Pricing

Expand Down
14 changes: 14 additions & 0 deletions src/utils/opening_hours.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,4 +258,18 @@ export function getUpcomingOpenTime(
}
}
return bestResult;
}

/**
* Temporary (until Place is GA) replacement for the built-in isOpen() method.
*/
export function isOpen(place: Place, now = new Date()): boolean|undefined {
if (!place.openingHours || place.utcOffsetMinutes == null) {
return undefined;
} else if (isPlaceAlwaysOpen(place.openingHours)) {
return true;
}
const {period} =
getCurrentPeriod(place.openingHours, place.utcOffsetMinutes, now);
return !!period;
}
39 changes: 38 additions & 1 deletion src/utils/opening_hours_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import {makeFakePlace} from '../testing/fake_place.js';

import {formatTimeWithWeekdayMaybe, getUpcomingCloseTime, getUpcomingOpenTime, isSoon, NextCloseTimeStatus, NextOpenTimeStatus} from './opening_hours.js';
import {formatTimeWithWeekdayMaybe, getUpcomingCloseTime, getUpcomingOpenTime, isOpen, isSoon, NextCloseTimeStatus, NextOpenTimeStatus} from './opening_hours.js';

type OpeningHours = google.maps.places.OpeningHours;
type OpeningHoursPeriod = google.maps.places.OpeningHoursPeriod;
Expand Down Expand Up @@ -322,4 +322,41 @@ describe('Opening hours utilities', () => {
});
});
});

describe('isOpen', () => {
it('returns undefined if opening hours are not available', () => {
const place = makeFakePlace({id: '123'});
expect(isOpen(place)).toBeUndefined();
});

it('returns true if the place is always open', () => {
const place = makeFakePlace(
{id: '123', openingHours: ALWAYS_OPEN_HOURS, utcOffsetMinutes: 0});
expect(isOpen(place)).toBeTrue();
});

it('returns true if the place is open now', () => {
const mondayNoonSf = makeDateInLocale('2023-08-07T12:00', SF_OFFSET);
const openingHours: OpeningHours = {
periods: [makePeriod(MON, 9, MON, 17)], // Wed 9am - 5pm
weekdayDescriptions: []
};
const place =
makeFakePlace({id: '123', openingHours, utcOffsetMinutes: SF_OFFSET});

expect(isOpen(place, mondayNoonSf)).toBeTrue();
});

it('returns false if the place is not open now', () => {
const mondayEarlySf = makeDateInLocale('2023-08-07T06:00', SF_OFFSET);
const openingHours: OpeningHours = {
periods: [makePeriod(MON, 9, MON, 17)], // Wed 9am - 5pm
weekdayDescriptions: []
};
const place =
makeFakePlace({id: '123', openingHours, utcOffsetMinutes: SF_OFFSET});

expect(isOpen(place, mondayEarlySf)).toBeFalse();
});
});
});
103 changes: 98 additions & 5 deletions src/utils/place_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {APILoader} from '../api_loader/api_loader.js';

import {extractTextAndURL} from './dom_utils.js';
import type {LatLng, LatLngLiteral, Place, PlaceResult, PriceLevel} from './googlemaps_types.js';
import {isOpen} from './opening_hours.js';

/**
* Returns true if `place` is a `PlaceResult`, and false if it's a `Place`.
Expand Down Expand Up @@ -110,18 +111,52 @@ export async function makePlaceFromPlaceResult(
typeof google.maps.places;
const place = new placesLibrary.Place(
{id: placeResult.place_id ?? 'PLACE_ID_MISSING'}) as Place;
const predefinedFields = convertToPlaceFields(placeResult);
let predefinedFields = convertToPlaceFields(placeResult);

// Override Place object's getters to return data from PlaceResult if defined.
return new Proxy(place, {
get(target, name, receiver) {
// Intercept calls to the `fetchFields()` method and filter out any field
// names in the request that already have values derived from PlaceResult.
if (name === 'fetchFields') {
return (request: google.maps.places.FetchFieldsRequest) => {
const forwardedFields = request.fields.filter(
(field) => predefinedFields[field as keyof Place] === undefined);
return target.fetchFields({...request, fields: forwardedFields});
return async (request: google.maps.places.FetchFieldsRequest) => {
const requestFields = request.fields as [keyof Place];
const forwardedFields = requestFields.filter(
(field) => predefinedFields[field] === undefined);
try {
return await target.fetchFields(
{...request, fields: forwardedFields});
} catch (e: unknown) {
// Place.fetchFields() is only available in beta versions of the
// Maps JS SDK. If a stable version of the SDK is loaded, fall
// back to the Place Details API.
if (isNotAvailableError(e, 'fetchFields()')) {
const placeResultFields =
mapPlaceFieldsToPlaceResultFields(forwardedFields);
if (!placeResultFields.length) return {place};
const response = await fetchFromPlaceDetails(
placesLibrary, place.id, placeResultFields);
predefinedFields = {
...convertToPlaceFields(response),
...predefinedFields,
};
return {place};
}
throw e;
}
};
} else if (name === 'isOpen') {
return async (d?: Date) => {
try {
// Must redirect the original isOpen() method's `this` to point to
// the proxy object.
return await Reflect.get(target, name, receiver).apply(receiver, [
d
]);
} catch (e: unknown) {
if (isNotAvailableError(e, 'isOpen()')) return isOpen(receiver, d);
throw e;
}
};
}
const value = predefinedFields[name as keyof Place];
Expand Down Expand Up @@ -271,3 +306,61 @@ function makeOpeningHoursPoint(
google.maps.places.OpeningHoursPoint {
return {day, hour: hours, minute: minutes};
}

const PLACE_TO_PLACE_RESULT_FIELDS:
Partial<Record<keyof Place, keyof PlaceResult>> = {
'addressComponents': 'address_components',
'adrFormatAddress': 'adr_address',
'businessStatus': 'business_status',
'formattedAddress': 'formatted_address',
'nationalPhoneNumber': 'formatted_phone_number',
'location': 'geometry',
'viewport': 'geometry',
'attributions': 'html_attributions',
'iconBackgroundColor': 'icon_background_color',
'svgIconMaskURI': 'icon_mask_base_uri',
'internationalPhoneNumber': 'international_phone_number',
'displayName': 'name',
'openingHours': 'opening_hours',
'photos': 'photos',
'plusCode': 'plus_code',
'priceLevel': 'price_level',
'rating': 'rating',
'reviews': 'reviews',
'types': 'types',
'googleMapsURI': 'url',
'userRatingCount': 'user_ratings_total',
'utcOffsetMinutes': 'utc_offset_minutes',
'websiteURI': 'website'
};

function mapPlaceFieldsToPlaceResultFields(fields: Array<keyof Place>):
Array<keyof PlaceResult> {
const placeResultFields: Array<keyof PlaceResult> = [];
for (const placeField of fields) {
const mapped = PLACE_TO_PLACE_RESULT_FIELDS[placeField];
if (mapped) placeResultFields.push(mapped);
}
return placeResultFields;
}

function isNotAvailableError(e: unknown, property: string): boolean {
const message = (e as Error).message || '';
return message.startsWith(`Place.prototype.${property} is not available`);
}

async function fetchFromPlaceDetails(
placesLibrary: typeof google.maps.places, placeId: string,
fields: string[]): Promise<PlaceResult> {
const placesService =
new placesLibrary.PlacesService(document.createElement('div'));
return new Promise((resolve, reject) => {
placesService.getDetails({placeId, fields}, (result, status) => {
if (result && status === 'OK') {
resolve(result);
} else {
reject(status);
}
});
});
}
75 changes: 75 additions & 0 deletions src/utils/place_utils_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,4 +242,79 @@ describe('makePlaceFromPlaceResult', () => {
{fields: ['displayName', 'location', 'photos', 'servesLunch']});
expect(fetchFieldsSpy).toHaveBeenCalledOnceWith({fields: ['servesLunch']});
});

it('uses the Places Details API when Place doesn\'t support fetchFields',
async () => {
const fetchFieldsSpy =
jasmine.createSpy('fetchFields')
.and.rejectWith(new Error(
'Place.prototype.fetchFields() is not available, try the beta channel.'));
const fakeGetDetails =
(options: google.maps.places.PlaceDetailsRequest,
callback: (
a: PlaceResult|null,
b: google.maps.places.PlacesServiceStatus) => void) => {
callback(
{price_level: 2},
'OK' as google.maps.places.PlacesServiceStatus);
};
const placeDetailsSpy =
jasmine.createSpy('placeDetails').and.callFake(fakeGetDetails);
const fakePlacesLibrary = {
Place: class {
constructor(options: google.maps.places.PlaceOptions) {
return makeFakePlace(
{id: options.id, fetchFields: fetchFieldsSpy});
}
},

PlacesService: class {
getDetails = placeDetailsSpy;
}
};
env.importLibrarySpy?.and.resolveTo(fakePlacesLibrary);

const place = await makePlaceFromPlaceResult(
{place_id: '123', url: 'http://foo/bar', rating: 3.5});

await place.fetchFields({fields: ['rating', 'priceLevel']});
expect(placeDetailsSpy)
.toHaveBeenCalledOnceWith(
{placeId: '123', fields: ['price_level']},
jasmine.any(Function));
expect(place.priceLevel)
.toEqual('MODERATE' as google.maps.places.PriceLevel);
});

it('calls the built-in isOpen method when available on Place', async () => {
const isOpenSpy = jasmine.createSpy('isOpen').and.resolveTo(true);
const fakePlacesLibrary = {
Place: class {
constructor(options: google.maps.places.PlaceOptions) {
return makeFakePlace({id: options.id, isOpen: isOpenSpy});
}
},
};
env.importLibrarySpy?.and.resolveTo(fakePlacesLibrary);
const place = await makePlaceFromPlaceResult({place_id: '123'});

expect(await place.isOpen()).toBe(true);
});

it('calls the utility isOpen function when isOpen() isn\'t implemented on Place',
async () => {
const isOpenSpy = jasmine.createSpy('isOpen').and.rejectWith(new Error(
'Place.prototype.isOpen() is not available, try the beta channel.'));
const fakePlacesLibrary = {
Place: class {
constructor(options: google.maps.places.PlaceOptions) {
return makeFakePlace({id: options.id, isOpen: isOpenSpy});
}
},
};
env.importLibrarySpy?.and.resolveTo(fakePlacesLibrary);
const place = await makePlaceFromPlaceResult({place_id: '123'});

expect(await place.isOpen()).toBe(undefined);
});
});

0 comments on commit 08a269b

Please sign in to comment.