How to display the route on ReactGoogleMap? - reactjs

Describe the bug 🐛
Using the Google Maps API I'm getting the directions and I'm creating DirectionsRenderer all looks correct, the map is displayed but the route is not displayed, not sure if this option is not available on GoogleMapReact
To Reproduce 🔍
Steps to reproduce the behavior:
Add the request to get the directions in the constructor
constructor(props: any){
super(props)
const DirectionsService = new google.maps.DirectionsService();
this.state = {};
DirectionsService.route(
{
origin: new google.maps.LatLng(19.5682414, -99.0436029),
destination: new google.maps.LatLng(19.7682414, -99.0436029),
travelMode: window.google.maps.TravelMode.DRIVING,
},
(result, status) => {
// console.log("status", status);
if (status === window.google.maps.DirectionsStatus.OK) {
this.setState({
directions: result,
});
console.log(result);
} else {
console.error(`error fetching directions ${result}`);
}
}
);
At this point, the directions are set in the state so let's try to render it.
render() {
return (
// Important! Always set the container height explicitly
<div id="BrainMap" className="schedulerMap justify-content-md-left">
<GoogleMapReact
bootstrapURLKeys={{ key: process.env.REACT_APP_MAPS_API_KEY }}
defaultCenter={this.initialPosition.center}
defaultZoom={this.initialPosition.zoom}
>
{this.state.directions && (
<DirectionsRenderer
directions={this.state.directions}
options={{
polylineOptions: {
strokeOpacity: 0.4,
strokeWeight: 4,
},
preserveViewport: true,
suppressMarkers: true,
}}
/>
)}
</GoogleMapReact>
</div>
);
}
Expected behavior 💭
I expect to see the route displayed on the map.
Other Context
In other examples looks like they are using other GoogleMap Component and other functions with composing, I will like to avoid this.
Code
import React, { Component } from "react";
import GoogleMapReact from "google-map-react";
import { DirectionsRenderer } from "react-google-maps";
import MapCard from "../commons/MapCard";
import "./Scheduler.css";
type BrainMapState = {
directions?: any;
}
class BrainMap extends Component {
state: BrainMapState;
initialPosition = {
center: {
lat: 19.4978,
lng: -99.1269,
},
zoom: 12,
};
constructor(props: any){
super(props)
const DirectionsService = new google.maps.DirectionsService();
this.state = {};
DirectionsService.route(
{
origin: new google.maps.LatLng(19.5682414, -99.0436029),
destination: new google.maps.LatLng(19.7682414, -99.0436029),
travelMode: window.google.maps.TravelMode.DRIVING,
},
(result, status) => {
// console.log("status", status);
if (status === window.google.maps.DirectionsStatus.OK) {
this.setState({
directions: result,
});
console.log(result);
} else {
console.error(`error fetching directions ${result}`);
}
}
);
//const directionsRenderer = new google.maps.DirectionsRenderer();
}
render() {
return (
// Important! Always set the container height explicitly
<div id="BrainMap" className="schedulerMap justify-content-md-left">
<GoogleMapReact
bootstrapURLKeys={{ key: process.env.REACT_APP_MAPS_API_KEY }}
defaultCenter={this.initialPosition.center}
defaultZoom={this.initialPosition.zoom}
>
{this.state.directions && (
<DirectionsRenderer
directions={this.state.directions}
options={{
polylineOptions: {
strokeOpacity: 0.4,
strokeWeight: 4,
},
preserveViewport: true,
suppressMarkers: true,
}}
/>
)}
</GoogleMapReact>
</div>
);
}
}
export default BrainMap;

Since GoogleMapReact library does not support Directions Service API, the following component could be introduced to render directions:
function DirectionsRenderer(props: {
map: google.maps.Map | null;
origin: google.maps.LatLngLiteral;
destination: google.maps.LatLngLiteral;
}) {
async function getRoute(
origin: google.maps.LatLngLiteral,
destination: google.maps.LatLngLiteral
): Promise<google.maps.DirectionsResult> {
const directionsService = new google.maps.DirectionsService();
return new Promise(function (resolve, reject) {
directionsService.route(
{
origin: origin,
destination: destination,
travelMode: google.maps.TravelMode.DRIVING,
},
(result: any, status: google.maps.DirectionsStatus) => {
if (status === google.maps.DirectionsStatus.OK) {
resolve(result);
} else {
reject(result);
}
}
);
});
}
async function renderRoute() {
const directions = await getRoute(props.origin, props.destination);
const directionsRenderer = new google.maps.DirectionsRenderer();
directionsRenderer.setMap(props.map);
directionsRenderer.setDirections(directions);
}
useEffect(() => {
renderRoute().catch((err) => {
console.log(err);
});
}, []);
return null;
}
Usage
function MapWithADirectionsRenderer(props: {
center: google.maps.LatLngLiteral;
zoom: number;
}) {
const [map, setMap] = useState<google.maps.Map | null>(null);
function handleApiLoaded(mapInstance: google.maps.Map, google: any) {
setMap(mapInstance);
}
return (
<div style={{ height: "100vh", width: "100%" }}>
<GoogleMapReact
bootstrapURLKeys={{ key: "--Google Maps Key goes here--" }}
defaultCenter={props.center}
defaultZoom={props.zoom}
onGoogleApiLoaded={({ map, maps }) => handleApiLoaded(map, maps)}
>
{map && (
<DirectionsRenderer
map={map}
origin={{ lat: 40.756795, lng: -73.954298 }}
destination={{ lat: 41.756795, lng: -78.954298 }}
/>
)}
</GoogleMapReact>
</div>
);
}
Demo
Note: Google Maps Key needs to be specified
Result

Related

How to add Google Maps Waypoints in react.js?

I am trying to implement Google Maps Directions Waypoints using #react-google-maps/api library to show the directions between starting and ending points.
With mock data coordinates it seems to work, but not with the data coming from the api/json file.
Here is the Codesandbox link
And the code below
import React, { useState } from "react";
import {
DirectionsRenderer,
DirectionsService,
GoogleMap,
LoadScript,
Marker
} from "#react-google-maps/api";
const containerStyle = {
width: "100%",
height: "900px"
};
const center = {
lat: 51.4332,
lng: 7.6616
};
const options = {
disableDefaultUI: true,
zoomControl: true,
fullscreenControl: true,
maxZoom: 17
};
export default function App({ parks }) {
const [directions, setDirections] = useState();
const directionsCallback = React.useCallback((response) => {
console.log(response);
if (response !== null) {
if (response.status === "OK") {
console.log("response", response);
setDirections(response);
} else {
console.log("response: ", response);
}
}
}, []);
//destination
const destinationPark = parks.features.find(
(park) => park.properties.id === 28007
);
//origin
const originPark = parks.features.find(
(park) => park.properties.id === 35299
);
const google = window.google;
// data to add as waypoints
const waypointsParks = parks.features.filter(
(park) => park.properties.id !== 28007 && park.properties.id !== 35299
);
return (
<div style={{ width: "100%", height: "100%" }}>
<LoadScript googleMapsApiKey={google_api_key}>
<GoogleMap
mapContainerStyle={containerStyle}
center={center}
zoom={6}
options={options}
>
<Marker
position={{
lat: parseFloat("52.50920109083271"),
lng: parseFloat("13.416411897460808")
}}
/>
)
<DirectionsService
callback={directionsCallback}
options={{
destination: `${destinationPark.properties.coordinates[1]}, ${destinationPark.properties.coordinates[0]}`,
origin: `${originPark.properties.coordinates[1]}, ${originPark.properties.coordinates[0]}`,
travelMode: "DRIVING"
// waypoints: [
// {
// location: new google.maps.LatLng(
// 52.596714626379296,
// 14.70278827986568
// )
// },
// {
// location: new google.maps.LatLng(
// 52.56193313494678,
// 11.52648542963747
// )
// }
// ]
}}
/>
<DirectionsRenderer
options={{
directions: directions
}}
/>
</GoogleMap>
</LoadScript>
</div>
);
}
Any help will be appreciated.
Assuming the failure is the following based upon codesandbox log.
Directions request returned no results.
Your application must handle the case where no results are found.
If you feel like this is a mistake, please provide a simplified example with just the origin, destination, waypoints, etc that you believe should return a result.

Limit the number of clicks on map using React

I am using the Google Maps API and I want to limit the number of markers on the map (limit: 10). I couldn't find anything related to it in the API docs
neither I can find any similar source to solve my problem.
Here is my code:
import React from "react";
import {
GoogleMap,
useLoadScript,
Marker,
} from "#react-google-maps/api";
const mapContainerStyle = {
height: "50vh",
width: "100vw",
};
const options = {
zoomControl: false,
scrollwheel: false,
draggable: false
};
const center = {
lat: 34.155834,
lng: -119.202789,
};
export default function App() {
const { isLoaded, loadError } = useLoadScript({
googleMapsApiKey: "AIzaSyCpaQDSgGTCetTR0uz42RyV80cByaGaYLs",
});
const [markers, setMarkers] = React.useState([]);
const onMapClick =
React.useCallback((e) => {
setMarkers((current) =>
[
...current,
{
lat: e.latLng.lat(),
lng: e.latLng.lng(),
},
]);
}, []);
const mapRef = React.useRef();
const onMapLoad = React.useCallback((map) => {
mapRef.current = map;
}, []);
if (loadError) return "Error";
if (!isLoaded) return "Loading...";
return (
<div>
<GoogleMap
id="map"
mapContainerStyle={mapContainerStyle}
zoom={14}
center={center}
options={options}
onClick={onMapClick}
onLoad={onMapLoad}
>
{markers.map((marker) => (
<Marker
key={`${marker.lat}-${marker.lng}`}
position={{ lat: marker.lat, lng: marker.lng }}
onClick={() => {
console.log("clicked")
}}
/>
))}
</GoogleMap>
</div>
);
}
How do I set the number of clicks up to 10?
You could do something like this. You may also want to call another function before returning current to perform some other update to alert the user they are maxed out on markers.
const onMapClick = React.useCallback((e) => {
setMarkers((current) => {
if (current.length < 10) {
return [
...current,
{
lat: e.latLng.lat(),
lng: e.latLng.lng()
}
];
} else {
return current;
};
});
}, []);

react and google Directions API without map

I need the results from .route() in my internal app, not going into any map. I need the duration and distance between the origin and destination for further calculations in my app.
So far I tried it with a callback func:
function getTravelTime(origin, destination, cb) {
const directionsService = new google.maps.DirectionsService();
directionsService.route(
{
origin: origin,
destination: destination,
travelMode: "DRIVING"
},
(result, status) => {
if (status === google.maps.DirectionsStatus.OK) {
cb(null, {
duration: moment.utc(moment.duration(result.routes[0].legs[0].duration.value, 'seconds').as('milliseconds')).format('HH:mm'),
distance: result.routes[0].legs[0].distance.value
});
} else {
cb('error');
console.log(result);
}
}
);
};
and I tried to read it like this:
let tInfo = getTravelTime(origin, destination, function (err, dist) {
if (!err) {
let distanceBetweenLocations = dist.distance;
let durationBetweenLocations = dist.duration;
// Or with saving to a state
setTravelInformation(prevState => ({
distance: dist.distance,
duration: dist.duration
}));
}
});
Is there any possibility to calculate distance and travel time without needing to render the map?
So far I got this, heavily shortened as I got much more logic for other components in the same file:
import {
withGoogleMap,
GoogleMap,
withScriptjs,
Marker,
DirectionsRenderer
} from "react-google-maps";
const getTravelTime = (origin, destination) => {
const directionsService = new google.maps.DirectionsService();
directionsService.route(
{
origin: origin,
destination: destination,
travelMode: google.maps.TravelMode.DRIVING
},
(result, status) => {
console.log(result)
if (status === google.maps.DirectionsStatus.OK) {
setDirections(result);
} else {
setError(result);
}
}
);
}
Do I need to use HoC withScriptjs and wrap my component around this?
You could use useState and useEffect, see https://reactjs.org/docs/hooks-effect.html
const [distance, setDistance] = useState(0);
const [duration, setDuration] = useState(0);
useEffect(() => {
if (distance && duration) {
console.log("Distance & Duration have updated", distance, duration);
}
}, [distance, duration]);
When you receive Directions results, update the distance and duration with whatever value you need:
directionsService.route({
origin: origin,
destination: destination,
travelMode: google.maps.TravelMode.DRIVING
},
(result, status) => {
if (status === google.maps.DirectionsStatus.OK) {
setDistance(result.routes[0].legs[0].distance.value);
setDuration(result.routes[0].legs[0].duration.value);
} else {
console.error("error fetching directions", result, status);
}
}
);
Here is a working snippet using #react-google-maps/api
https://codesandbox.io/s/react-google-mapsapi-directions-service-m7qif
You need to use a valid API key in case it doesn't work.
import React, { Component } from "react";
import { withGoogleMap, GoogleMap, withScriptjs, DirectionsRenderer, Marker } from "react-google-maps";
import { compose, withProps } from "recompose";
import PropTypes from "prop-types";
class MapDirectionsRenderer extends Component {
static propTypes = {
waypoints: PropTypes.array,
places: PropTypes.array
};
state = {
directionsRef: '',
directions: null,
error: null
};
componentDidMount() {
const { places, origDest } = this.props;
const waypointsArray = places.map(p => ({
location: p.geocode,
stopover: true
}));
const origin = origDest.origin_geocode;
const destination = origDest.destination_geocode;
const directionsService = new window.google.maps.DirectionsService();
directionsService.route(
{
origin: origin,
destination: destination,
travelMode: window.google.maps.TravelMode.DRIVING,
waypoints: waypointsArray.length >= 1 ? waypointsArray : [],
},
(result, status) => {
if (status === window.google.maps.DirectionsStatus.OK) {
this.setState({
directions: result,
});
}
}
);
}
render() {
return (
this.state.directions && (
<DirectionsRenderer
directions={this.state.directions}
/>
)
);
}
}
const MapView = compose(
withProps({
googleMapURL: "https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY_HERE&v=3.exp",
loadingElement: <div style={{ height: `100%` }} />,
containerElement: <div style={{ height: `300px`, width: '100%' }} />,
mapElement: <div style={{ height: `100%` }} />
}),
withScriptjs,
withGoogleMap
)(props => (
<GoogleMap
key={props.travelMode}
defaultZoom={12}
center={{ lat: 0, lng: 0 }}
>
<MapDirectionsRenderer
places={props.wayPoints}
origDest={props.originDestination}
/>
</GoogleMap >
));
export default MapView;

How to call another function from withHandlers?

I am trying to implement google maps in my app using react-google-maps package. In map, I am showing multiple Marker and used MarkerCluster.
Until now I have no issue and easily implemented from the doc. But now I want to show InfoWindow if the marker is clicked.
So, I thought of making a function to get the click event and pass the markerId, so I can call the API and get the relevant data for that marker and then put it in infowindow in a tabular manner.
Now, the problem I am facing is:
1) Calling onToggleOpen from onMarkerClick
2) how to set data in infowindow object in onMarkerClick
All this problem I am facing is because i am using HOC i.e recompose.
I am used to Class implement but tried functional implement trying to make it purely stateless.
Reference link: https://tomchentw.github.io/react-google-maps/#infowindow
Following is my code:
import React, { Component } from "react";
import Header from "./Header.js";
import Sidebar from "./Sidebar.js";
import axios from "axios";
import imgmapcluster from "./pins/iconmapcluster.png";
import user from "./pins/user1copy.png";
import { compose, withProps, withHandlers } from "recompose";
import {
withScriptjs,
withGoogleMap,
GoogleMap,
Marker,
InfoWindow
} from "react-google-maps";
// const fetch = require("isomorphic-fetch");
const {
MarkerClusterer
} = require("react-google-maps/lib/components/addons/MarkerClusterer");
const MapWithAMarkerClusterer = compose(
withProps({
googleMapURL:
"https://maps.googleapis.com/maps/api/js?key=AIzaSyCHi5ryWgN1FcZI-Hmqw3AdxJQmpopYJGk&v=3.exp&libraries=geometry,drawing,places",
loadingElement: <div style={{ height: `100%` }} />,
containerElement: <div style={{ height: `90vh` }} />,
mapElement: <div style={{ height: `100%` }} />
}),
withHandlers(
{
onMarkerClustererClick: () => markerClusterer => {
// console.log("markerCluster", markerClusterer);
const clickedMarkers = markerClusterer.getMarkers();
// console.log(`Current clicked markers length: ${clickedMarkers.length}`);
// console.log(clickedMarkers);
},
onMarkerClick: (props) => markerss => {
//calling api and setting info window object
props.isOpen=!props.isOpen //showing error
},
onToggleOpen: ({ isOpen }) => () => ({
isOpen: !isOpen
})
}
),
withScriptjs,
withGoogleMap
)(props => (
<GoogleMap
defaultZoom={5}
defaultCenter={{ lat: 22.845625996700075, lng: 78.9629 }}
>
<MarkerClusterer
onClick={props.onMarkerClustererClick}
averageCenter
styles={[
{
textColor: 'white',
url: imgmapcluster,
height: 68,
lineHeight: 3,
width: 70
}
]}
enableRetinaIcons
gridSize={50}
>
{props.markers.map((marker, index) => (
<Marker
key={index}
icon={user}
onClick={props.onMarkerClick.bind(props,marker)}
position={{ lat: marker.latitude, lng: marker.longitude }}
/>
))}
{props.isOpen && (
<InfoWindow
// position={{ lat: props.infowindow.lat, lng: props.infowindow.lng }}
onCloseClick={props.onToggleOpen}
>
<h4>hello</h4>
</InfoWindow>
)}
</MarkerClusterer>
</GoogleMap>
));
class DemoApp extends React.PureComponent {
componentWillMount() {
this.setState({ markers: [],isOpen:false,infowindow:{} });
}
componentDidMount() {
axios({
url: "http://staging.clarolabs.in:6067/farmerinfo/farmercoordinates",
method: "POST",
data: {
temp: "temp"
},
headers: {
"Content-Type": "application/json"
}
}).then(res => {
this.setState({ markers: res.data.data.list });
});
}
render() {
return <MapWithAMarkerClusterer markers={this.state.markers} isOpen={this.state.isOpen} InfoWindowobject={this.state.InfoWindowobject}/>;
}
}
In order to call a withHandler from another, you need to separate them within two handlers. Also you can make use of withStateHandler and store the infoWindow state
withStateHandlers(
{ infoWindow: null },
{
setInfoWindow: () => (value) => ({ infoWindow: value)
}
),
withHandlers({
onToggleOpen: ({ isOpen }) => () => ({
isOpen: !isOpen
})
}),
withHandlers(
{
onMarkerClustererClick: () => markerClusterer => {
// console.log("markerCluster", markerClusterer);
const clickedMarkers = markerClusterer.getMarkers();
// console.log(`Current clicked markers length: ${clickedMarkers.length}`);
// console.log(clickedMarkers);
},
onMarkerClick: (props) => markerss => {
const { setInfoWindow, onToggleOpen} = props;
//calling api and setting info window object
setInfoWindow({lat: res.lat, lng: res.lng}) // set infoWindow object here
onToggleOpen() // Toggle open state
}
}
),
You probably should use withStateHandlers to handle state. Also, you can write withHandlers more than once, so handlers inside following withHandlers will have access to other handlers.
compose(
// .....
withStateHandlers(
{ isOpen: false },
{
toggleOpen: (state) => () => ({ isOpen: !state.isOpen }),
// or
setOpen: () => (value) => ({ isOpen: value }),
}
),
withHandlers(
// .....
{
onMarkerClick: (props) => markerss => {
// .....
props.toggleOpen();
// .....
},
},
// .....
),
// .....
)

google.maps.places.PlacesService(map) always returns null

Am trying to fetch nearby restaurants using google maps API with react-google-maps.
import React from 'react';
import { compose, withState, withProps, withStateHandlers, lifecycle } from 'recompose';
import { withScriptjs, withGoogleMap, GoogleMap, Marker, InfoWindow } from 'react-google-maps';
import dotenv from 'dotenv';
import { HOME_MAP } from './MapNavigationConstants';
import MapSearch from './MapSearch';
dotenv.config();
const MapWithInfoWindow = compose(
withProps({
googleMapURL: HOME_MAP,
loadingElement: <div style={{ height: `100%` }} />,
containerElement:<div style={{ height: `720px` }} />,
mapElement:<div style={{ height: `100%` }} />,
}),
withState('mapUrl', 'setMapUrl', ''),
withState('bounds', 'setBounds', null),
withState('center', 'setCenter', {
lat: 53.3498, lng: -6.2603
}),
withState('markers', 'setMarkers', [
{
position: {
lat: () => 53.3498,
lng: () => -6.2603
}
}
]),
withState('places', 'updatePlaces', ''),
withStateHandlers(() => ({
isOpen: false,
isExploreOn: false,
}), {
onToggleOpen: ({ isOpen }) => () => ({
isOpen: !isOpen,
})
}
),
lifecycle({
componentWillMount() {
const refs = {}
this.setState({
onMapMounted: ref => {
refs.map = ref;
},
onBoundsChanged: (bounds, center, markers) => {
this.props.setBounds(!bounds ? this.props.bounds : bounds);
this.props.setCenter(!center ? this.props.center : center);
this.props.setMarkers(!markers ? this.props.markers : markers);
},
fetchPlaces: () => {
this.props.setMapUrl('places');
const bounds = refs.map.getBounds();
const map = refs.map;
const service = new window.google.maps.places.PlacesService(map);
console.log(service);
const request = {
bounds,
type: ['restaurants', 'cafe','bars']
};
service.nearBySearch(request, (results, status) => {
if (status === window.google.maps.places.PlacesServiceStatus.OK) {
this.props.updatePlaces(results);
}
});
}
})
},
}),
withScriptjs,
withGoogleMap)((props) =>
<GoogleMap
ref={props.onMapMounted}
defaultCenter = {props.center}
defaultZoom = { 13 }
center={props.center}
bounds={props.bounds}
options={{gestureHandling: 'cooperative',
scrollwheel: false,
disableDefaultUI: true,
}}
bootstrapURLKeys={{libraries: props.mapUrl}}
onBoundsChanged={props.onBoundsChanged}
>
<MapSearch
onBoundsChanged={(bounds, center, markers) => props.onBoundsChanged(bounds, center, markers)}
fetchPlaces={props.fetchPlaces}
/>
{
props.markers && props.markers.length > 0 && props.markers.map((marker, index) => (
<Marker
key={index}
position={{ lat: marker.position.lat(), lng:marker.position.lng() }}
onClick={props.onToggleOpen}
>
{props.isOpen && <InfoWindow onCloseClick={props.onToggleOpen}>
{props.children}
</InfoWindow>}
</Marker>
))
}{
props.places && props.places.length > 0 && props.places.map((place, index) => (
<Marker
key={index}
position={{ lat: place.location.lat(), lng:place.location.lng() }}
onClick={props.onToggleOpen}
>
{props.isOpen && <InfoWindow onCloseClick={props.onToggleOpen}>
{props.children}
</InfoWindow>}
</Marker>
))
}
</GoogleMap>
)
export default MapWithInfoWindow;
here HOME_MAP = https://maps.googleapis.com/maps/api/js?key=${KEY}&v=3.exp&libraries=geometry,drawing,places
Inside fetchplaces method, new window.google.maps.places.PlacesService(map) always returns null and service.nearBySearch throws not a function error.
Please help.
There are at least two issues with your example
const map = refs.map;
const service = new window.google.maps.places.PlacesService(map);
^^^
map object here corresponds to the instance of Map component while google.maps.places.PlacesService class expects Google Maps object instead. In case of react-google-maps library PlacesService could be instantiated like this:
mapMounted(element) {
const mapObject = element.context[MAP];
const service = new google.maps.places.PlacesService(map);
//...
}
where
<GoogleMap
ref={this.mapMounted}
defaultZoom={this.props.zoom}
defaultCenter={this.props.center}/>
There is also a typo at line:
service.nearBySearch(request, (results, status) => {
^^^^^^^^^^^^
function should be renamed to nearbySearch
Here is a demo that demonstrates how to utilize Places Service with react-google-maps library.

Resources