Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions examples/places-ui-kit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@

![image](https://user-images.githubusercontent.com/39244966/208682692-d5b23518-9e51-4a87-8121-29f71e41c777.png)

This is an example to show how to setup the Google Places UI Kit (Preview) webcomponents
This is an example to show how to setup the Google Places UI Kit webcomponents

## Enable the APIs
## Enable the API

To use the Places UI Kit you need to enable the [API][places-ui-kit] in the Cloud Console.
Additionally for the elevation component you need the [Elevation API][elevation-api]

## Google Maps Platform API Key

Expand Down
203 changes: 161 additions & 42 deletions examples/places-ui-kit/src/app.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import React, {useState, useMemo} from 'react';
import {
APIProvider,
Map,
useMap,
useMapsLibrary
} from '@vis.gl/react-google-maps';
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import {createRoot} from 'react-dom/client';
import {APIProvider, Map} from '@vis.gl/react-google-maps';

import {PlaceDetailsMarker} from './components/place-details-marker';
import {PlaceSearchWebComponent} from './components/place-search-webcomponent';
import {SearchBar} from './components/search-bar';
import {BasicPlaceAutocomplete} from './components/basic-place-autocomplete';
import {NearbySearchOptions, PlaceSearch} from './components/place-search';

import ControlPanel from './control-panel';
import PlaceDetailsMarker from './place-details-marker';

import './styles.css';

Expand All @@ -17,11 +22,12 @@ if (!API_KEY) {
console.error('Missing Google Maps API key');
}

export type PlaceType =
| 'restaurant'
| 'cafe'
| 'electric_vehicle_charging_station'
| null;
type PlaceType = 'restaurant' | 'cafe' | 'electric_vehicle_charging_station';

type PlaceTypeOption = {
value: PlaceType;
label: string;
};

export type DetailsSize = 'FULL' | 'COMPACT';
export type ColorScheme = 'light' | 'dark';
Expand All @@ -36,13 +42,98 @@ const MAP_CONFIG = {
};

const App = () => {
// APIProvider sets up the Google Maps JavaScript API with the specified key
return (
<APIProvider apiKey={API_KEY}>
<Layout />
</APIProvider>
);
};

const Layout = () => {
const [places, setPlaces] = useState<google.maps.places.Place[]>([]);
const [selectedPlaceId, setSelectedPlaceId] = useState<string | null>(null);
const [locationId, setLocationId] = useState<string | null>(null);
const [placeType, setPlaceType] = useState<PlaceType>('restaurant');
const [detailsSize, setDetailsSize] = useState<DetailsSize>('FULL');
const [colorScheme, setColorScheme] = useState<ColorScheme>('light');

const geoLib = useMapsLibrary('geometry');

const map = useMap();

const [nearbySearch, setNearbySearch] = useState<
NearbySearchOptions | undefined
>();

const placeTypeOptions: PlaceTypeOption[] = useMemo(
() => [
{value: 'restaurant', label: 'Restaurants'},
{value: 'cafe', label: 'Cafes'},
{value: 'electric_vehicle_charging_station', label: 'EV Charging'}
],
[]
);

// Calculate a circular region based on the map's current bounds
// This is used to restrict the places search to the visible map area
const getContainingCircle = useCallback(
(bounds?: google.maps.LatLngBounds): google.maps.CircleLiteral => {
if (!bounds || !geoLib)
return {center: MAP_CONFIG.defaultCenter, radius: 2800};

// Calculate diameter between the northeast and southwest corners of the bounds
const diameter = geoLib.spherical.computeDistanceBetween(
bounds.getNorthEast(),
bounds.getSouthWest()
);
const calculatedRadius = diameter / 2;

// Cap the radius at 50km to avoid exceeding Google Maps API limits
const cappedRadius = Math.min(calculatedRadius, 50000);
return {center: bounds.getCenter(), radius: cappedRadius};
},
[geoLib]
);

const handlePlaceSelect = useCallback(
async (place: google.maps.places.Place) => {
try {
// Fetch viewport data for the selected place
await place.fetchFields({
fields: ['viewport']
});

// If the place has a viewport (area boundaries), adjust the map to show it
if (place?.viewport) {
map?.fitBounds(place.viewport);
}

setLocationId(place.id);
} catch (error) {
console.error('Error fetching place fields:', error);
setLocationId(null);
}
},
[map, setLocationId]
);

// On location change via autocomplete update nearbysearch options for the place search component
useEffect(() => {
if (!geoLib || !map) {
return;
}
const bounds = map.getBounds();
const circle = getContainingCircle(bounds);

if (!circle) return;

setNearbySearch({
locationRestriction: circle,
includedPrimaryTypes: placeType ? [placeType] : undefined
});
}, [geoLib, map, placeType, getContainingCircle, locationId]);

// Memoize the place markers to prevent unnecessary re-renders
// Only recreate when places, selection, or details size changes
const placeMarkers = useMemo(() => {
Expand All @@ -58,57 +149,85 @@ const App = () => {
}, [places, selectedPlaceId, detailsSize]);

return (
// APIProvider sets up the Google Maps JavaScript API with the specified key
// Using 'alpha' version to access the latest features including UI Kit components
<APIProvider apiKey={API_KEY}>
<div className="places-ui-kit" style={{colorScheme: colorScheme}}>
<div className="place-list-wrapper">
<div className="places-ui-kit" style={{colorScheme: colorScheme}}>
<div className="place-list-wrapper">
<div className="place-list-container">
{/*
PlaceSearchtWebComponent displays a list of places based on:
The PlaceSearch component displays a list of places based on:
- The selected place type (restaurant, cafe, etc.)
- The current map location and bounds
*/}
<PlaceSearchWebComponent
placeType={placeType}
locationId={locationId}
setPlaces={setPlaces}
onPlaceSelect={place => setSelectedPlaceId(place?.id ?? null)}
<PlaceSearch
contentConfig="standard"
selectable
truncationPreferred
nearbySearch={nearbySearch}
onLoad={e => setPlaces(e.target.places)}
onSelect={e => setSelectedPlaceId(e.place?.id ?? null)}
/>
</div>

<div className="map-container">
{/*
</div>
<div className="map-container">
{/*
The Map component renders the Google Map
Clicking on the map background will deselect any selected place
*/}
<Map {...MAP_CONFIG} onClick={() => setSelectedPlaceId(null)}>
{placeMarkers}
</Map>
<Map {...MAP_CONFIG} onClick={() => setSelectedPlaceId(null)}>
{placeMarkers}
</Map>

{/*
{/*
SearchBar allows users to:
- Select the type of place they want to find
- Search for a specific location to center the map on
*/}
<SearchBar
placeType={placeType}
setPlaceType={setPlaceType}
setLocationId={setLocationId}
/>
<div
className="autocomplete-wrapper"
role="search"
aria-label="Location search">
<label htmlFor="place-type-select">Find</label>
<select
id="place-type-select"
value={placeType ?? ''}
onChange={e => setPlaceType(e.target.value as PlaceType)}>
{placeTypeOptions.map(option => (
<option key={option.value} value={option.value || ''}>
{option.label}
</option>
))}
</select>
<span>near</span>
<div className="autocomplete-container">
{/*
The BasicAutocomplete component allows users to search for a place and returns its place id
*/}
<BasicPlaceAutocomplete
onSelect={e => handlePlaceSelect(e.place)}
style={{borderRadius: '1rem'}}>
<svg
xmlns="http://www.w3.org/2000/svg"
height="24px"
viewBox="0 -960 960 960"
width="24px"
fill="#e3e3e3">
<path d="M480-280q83 0 141.5-58.5T680-480q0-83-58.5-141.5T480-680q-83 0-141.5 58.5T280-480q0 83 58.5 141.5T480-280Zm0 200q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Z" />
</svg>
</BasicPlaceAutocomplete>
</div>
</div>

{/*
{/*
ControlPanel provides UI controls for adjusting the size of place details
displayed in the InfoWindow
*/}
<ControlPanel
detailsSize={detailsSize}
onDetailSizeChange={setDetailsSize}
colorScheme={colorScheme}
onColorSchemeChange={setColorScheme}
/>
</div>
<ControlPanel
detailsSize={detailsSize}
onDetailSizeChange={setDetailsSize}
colorScheme={colorScheme}
onColorSchemeChange={setColorScheme}
/>
</div>
</APIProvider>
</div>
);
};
export default App;
Expand Down

This file was deleted.

Loading