diff --git a/CHANGELOG.md b/CHANGELOG.md index b8f5098..8aefcbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # `react-native-maps-directions` Changelog +## 1.9.1 – 2023-03-15 + +- Add `avoidTolls` option; +- Add `avoidHighways` option; +- Add `avoidFerries` option; + ## 1.9.0 – 2022-07-27 - Fix: `timePrecision` prop type to `MapViewDirectionsTimePrecision` @@ -46,7 +52,6 @@ - Return “Duration with traffic” as `duration` - Return `fare` in `onReady` - ## 1.6.0 - 2018-03-09 - Add `directionsServiceBaseUrl` prop to allow customisation of service to use. diff --git a/README.md b/README.md index 63e636d..2603c6c 100644 --- a/README.md +++ b/README.md @@ -56,22 +56,26 @@ Once the directions in between `destination` and `origin` has been fetched, a `M ### Props -| Prop | Type | Default | Note -|---|---|---|---| -| `origin` | `LatLng` or `String` | | The origin location to start routing from. -| `destination` | `LatLng` or `String` | | The destination location to start routing to. -| `apikey` | `String` | | Your Google Maps API Key _(request one [here](https://developers.google.com/maps/documentation/directions/get-api-key); if you're using an existing Google Maps API Key make sure you've enabled the Google Maps Directions API for that key using the [Google API Console](https://console.developers.google.com/apis/) by hitting the “Enable APIs and Services“ button)_. -| `waypoints` | [`LatLng` or `String`] | `[]` | Array of waypoints to use between origin and destination. -| `language` | `String` | `"en"` | The language to use when calculating directions. See [here](https://developers.google.com/maps/documentation/javascript/localization) for more info. -| `mode` | `String` | `"DRIVING"` | Which transportation mode to use when calculating directions. Allowed values are `"DRIVING"`, `"BICYCLING"`, `"WALKING"`, and `"TRANSIT"`. _(See [here](https://developers.google.com/maps/documentation/javascript/examples/directions-travel-modes) for more info)_. -| `resetOnChange` | `boolean` | `true` | Tweak if the rendered `MapView.Polyline` should reset or not when calculating the route between `origin` and `destionation`. Set to `false` if you see the directions line glitching. -| `optimizeWaypoints` | `boolean` | `false` | Set it to true if you would like Google Maps to re-order all the waypoints to optimize the route for the fastest route. Please be aware that if this option is enabled, you will be billed at a higher rate by Google as stated [here](https://developers.google.com/maps/documentation/javascript/directions#Waypoints). -| `splitWaypoints` | `boolean` | `false` | Directions API has a [limit](https://developers.google.com/maps/documentation/directions/usage-and-billing#directions-advanced) of 10 or 25 (depends on the billing plan) waypoints per route. When exceeding this limit you will be billed at a higher reate by Google. Set this to `true` if you would like to automatically split waypoints into multiple routes, thus bypassing this waypoints limit. -| `directionsServiceBaseUrl` | `string` | _(Google's)_ | Base URL of the Directions Service (API) you are using. By default the Google Directions API is used (`"https://maps.googleapis.com/maps/api/directions/json"`). Usually you won't need to change this. -| `region` | `String` | | If you are using strings for **origin** or **destination**, sometimes you will get an incorrect route because Google Maps API needs the region where this places belong to. See [here](https://developers.google.com/maps/documentation/javascript/localization#Region) for more info. -| `precision` | `String` | `"low"` | The precision level of detail of the drawn polyline. Allowed values are "high", and "low". Setting to "low" will yield a polyline that is an approximate (smoothed) path of the resulting directions. Setting to "high" may cause a hit in performance in case a complex route is returned. -| `timePrecision` | `String` | `"none"` | The timePrecision to get Realtime traffic info. Allowed values are "none", and "now". Defaults to "none". -| `channel` | `String` | `null` | If you include the channel parameter in your requests, you can generate a Successful Requests report that shows a breakdown of your application's API requests across different applications that use the same client ID (such as externally facing access vs. internally facing access). +| Prop | Type | Default | Note | +| -------------------------- | ---------------------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `origin` | `LatLng` or `String` | | The origin location to start routing from. | +| `destination` | `LatLng` or `String` | | The destination location to start routing to. | +| `apikey` | `String` | | Your Google Maps API Key _(request one [here](https://developers.google.com/maps/documentation/directions/get-api-key); if you're using an existing Google Maps API Key make sure you've enabled the Google Maps Directions API for that key using the [Google API Console](https://console.developers.google.com/apis/) by hitting the “Enable APIs and Services“ button)_. | +| `waypoints` | [`LatLng` or `String`] | `[]` | Array of waypoints to use between origin and destination. | +| `language` | `String` | `"en"` | The language to use when calculating directions. See [here](https://developers.google.com/maps/documentation/javascript/localization) for more info. | +| `mode` | `String` | `"DRIVING"` | Which transportation mode to use when calculating directions. Allowed values are `"DRIVING"`, `"BICYCLING"`, `"WALKING"`, and `"TRANSIT"`. _(See [here](https://developers.google.com/maps/documentation/javascript/examples/directions-travel-modes) for more info)_. | +| `resetOnChange` | `boolean` | `true` | Tweak if the rendered `MapView.Polyline` should reset or not when calculating the route between `origin` and `destionation`. Set to `false` if you see the directions line glitching. | +| `optimizeWaypoints` | `boolean` | `false` | Set it to true if you would like Google Maps to re-order all the waypoints to optimize the route for the fastest route. Please be aware that if this option is enabled, you will be billed at a higher rate by Google as stated [here](https://developers.google.com/maps/documentation/javascript/directions#Waypoints). | +| `splitWaypoints` | `boolean` | `false` | Directions API has a [limit](https://developers.google.com/maps/documentation/directions/usage-and-billing#directions-advanced) of 10 or 25 (depends on the billing plan) waypoints per route. When exceeding this limit you will be billed at a higher reate by Google. Set this to `true` if you would like to automatically split waypoints into multiple routes, thus bypassing this waypoints limit. | +| `directionsServiceBaseUrl` | `string` | _(Google's)_ | Base URL of the Directions Service (API) you are using. By default the Google Directions API is used (`"https://maps.googleapis.com/maps/api/directions/json"`). Usually you won't need to change this. | +| `region` | `String` | | If you are using strings for **origin** or **destination**, sometimes you will get an incorrect route because Google Maps API needs the region where this places belong to. See [here](https://developers.google.com/maps/documentation/javascript/localization#Region) for more info. | +| `precision` | `String` | `"low"` | The precision level of detail of the drawn polyline. Allowed values are "high", and "low". Setting to "low" will yield a polyline that is an approximate (smoothed) path of the resulting directions. Setting to "high" may cause a hit in performance in case a complex route is returned. | +| `timePrecision` | `String` | `"none"` | The timePrecision to get Realtime traffic info. Allowed values are "none", and "now". Defaults to "none". | +| `channel` | `String` | `null` | If you include the channel parameter in your requests, you can generate a Successful Requests report that shows a breakdown of your application's API requests across different applications that use the same client ID (such as externally facing access vs. internally facing access). | +| `avoidTolls` | `boolean` | `false` | Set to `true` if you want direction routing to avoid Tollways when possible. Can be combined with other 'avoid' parameters. There might be instances that setting `avoidTolls` may be anough to avoid directions passing both tollways and highways, so just test which parameters apply to your application. | +| `avoidHighways` | `boolean` | `false` | Set to `true` if you want direction routing to avoid Highways when possible. | +| `avoidFerries` | `boolean` | `false` | Set to `true` if you want direction routing to avoid Ferries when possible. | + #### More props Since the result rendered on screen is a `MapView.Polyline` component, all [`MapView.Polyline` props](https://github.com/airbnb/react-native-maps/blob/master/docs/polyline.md#props) – except for `coordinates` – are also accepted. @@ -93,7 +97,7 @@ Since the result rendered on screen is a `MapView.Polyline` component, all [`Map The values for the `origin` and `destination` props can take several forms. They can either be: - Coordinates in the form of an object with `latitude` and `longitude` keys -- Coordinates in the form of a string with `latitude` and `longitude` values separated by a comma +- Coordinates in the form of a string with `latitude` and `longitude` values separated by a comma - Strings representing an address - Strings representing a location - Strings containing a Place Id from the Google Maps Place API prefixed with `place_id:` @@ -114,35 +118,34 @@ Tip: Don't forget to tweak the `language` prop when using localized location nam ### Events/Callbacks -| Event Name | Returns | Notes -|---|---|---| -| `onStart` | `{ origin, destination, waypoints: [] }` | Callback that is called when the routing has started. -| `onReady` | `{ distance: Number, duration: Number, coordinates: [], fare: Object, waypointOrder: [[]] }` | Callback that is called when the routing has succesfully finished. Note: distance returned in kilometers and duration in minutes. -| `onError` | `errorMessage` | Callback that is called in case the routing has failed. +| Event Name | Returns | Notes | +| ---------- | -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| `onStart` | `{ origin, destination, waypoints: [] }` | Callback that is called when the routing has started. | +| `onReady` | `{ distance: Number, duration: Number, coordinates: [], fare: Object, waypointOrder: [[]] }` | Callback that is called when the routing has succesfully finished. Note: distance returned in kilometers and duration in minutes. | +| `onError` | `errorMessage` | Callback that is called in case the routing has failed. | ## Extended Example This example will draw a route between AirBnB's Office and Apple's HQ ```js -import React, { Component } from 'react'; -import { Dimensions, StyleSheet } from 'react-native'; -import MapView from 'react-native-maps'; -import MapViewDirections from 'react-native-maps-directions'; +import React, { Component } from 'react' +import { Dimensions, StyleSheet } from 'react-native' +import MapView from 'react-native-maps' +import MapViewDirections from 'react-native-maps-directions' -const { width, height } = Dimensions.get('window'); -const ASPECT_RATIO = width / height; -const LATITUDE = 37.771707; -const LONGITUDE = -122.4053769; -const LATITUDE_DELTA = 0.0922; -const LONGITUDE_DELTA = LATITUDE_DELTA * ASPECT_RATIO; +const { width, height } = Dimensions.get('window') +const ASPECT_RATIO = width / height +const LATITUDE = 37.771707 +const LONGITUDE = -122.4053769 +const LATITUDE_DELTA = 0.0922 +const LONGITUDE_DELTA = LATITUDE_DELTA * ASPECT_RATIO -const GOOGLE_MAPS_APIKEY = '…'; +const GOOGLE_MAPS_APIKEY = '…' class Example extends Component { - constructor(props) { - super(props); + super(props) // AirBnB's Office, and Apple Park this.state = { @@ -156,18 +159,15 @@ class Example extends Component { longitude: -122.4053769, }, ], - }; + } - this.mapView = null; + this.mapView = null } onMapPress = (e) => { this.setState({ - coordinates: [ - ...this.state.coordinates, - e.nativeEvent.coordinate, - ], - }); + coordinates: [...this.state.coordinates, e.nativeEvent.coordinate], + }) } render() { @@ -180,48 +180,59 @@ class Example extends Component { longitudeDelta: LONGITUDE_DELTA, }} style={StyleSheet.absoluteFill} - ref={c => this.mapView = c} + ref={(c) => (this.mapView = c)} onPress={this.onMapPress} > - {this.state.coordinates.map((coordinate, index) => + {this.state.coordinates.map((coordinate, index) => ( - )} - {(this.state.coordinates.length >= 2) && ( + ))} + {this.state.coordinates.length >= 2 && ( 2) ? this.state.coordinates.slice(1, -1): undefined} - destination={this.state.coordinates[this.state.coordinates.length-1]} + waypoints={ + this.state.coordinates.length > 2 + ? this.state.coordinates.slice(1, -1) + : undefined + } + destination={ + this.state.coordinates[this.state.coordinates.length - 1] + } apikey={GOOGLE_MAPS_APIKEY} strokeWidth={3} - strokeColor="hotpink" + strokeColor='hotpink' optimizeWaypoints={true} onStart={(params) => { - console.log(`Started routing between "${params.origin}" and "${params.destination}"`); + console.log( + `Started routing between "${params.origin}" and "${params.destination}"` + ) }} - onReady={result => { + onReady={(result) => { console.log(`Distance: ${result.distance} km`) console.log(`Duration: ${result.duration} min.`) this.mapView.fitToCoordinates(result.coordinates, { edgePadding: { - right: (width / 20), - bottom: (height / 20), - left: (width / 20), - top: (height / 20), - } - }); + right: width / 20, + bottom: height / 20, + left: width / 20, + top: height / 20, + }, + }) }} onError={(errorMessage) => { // console.log('GOT AN ERROR'); }} + avoidTolls={true} + avoidFerries={true} + avoidHighways={true} /> )} - ); + ) } } -export default Example; +export default Example ``` ## Example App diff --git a/index.d.ts b/index.d.ts index b19991a..62f7d0c 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,4 +1,4 @@ -declare module "react-native-maps-directions" { +declare module 'react-native-maps-directions' { // Type definitions for react-native-maps-directions 1.6 // Project: https://github.com/bramus/react-native-maps-directions // Definitions by: Ali Oguzhan Yildiz @@ -6,153 +6,85 @@ declare module "react-native-maps-directions" { // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped // TypeScript Version: 3.3 - import * as React from "react"; - - export type MapDirectionsLegs =[ - { - distance: { - text: string, - value: number - }, - duration: { - text: string, - value: number - }, - end_address: string, - end_location: { - lat: number, - lng: number - }, - start_address: string, - start_location: { - lat: number, - lng: number - }, - steps: [{ - distance: { - text: string, - value: number - }, - duration: { - text: string, - value: number - }, - end_location: { - lat: number, - lng: number - }, - start_location: { - lat: number, - lng: number - }, - html_instructions: string, - polyline: { - points: string - }, - travel_mode: string, - maneuver: string | undefined - }], - traffic_speed_entry: [], - via_waypoint: [], - }] - - export type MapDirectionsResponse = { - coordinates: [ - { - latitude: number, - longitude: number - }], - distance: number, - duration: number, - fares: [], - legs: MapDirectionsLegs, - waypointOrder: [[number]] - } - - - + import * as React from 'react' export type MapViewDirectionsOrigin = | string | { - latitude: number; - longitude: number; - }; + latitude: number + longitude: number + } export type MapViewDirectionsWaypoints = | string | { - latitude: number; - longitude: number; - }; + latitude: number + longitude: number + } export type MapViewDirectionsDestination = | string | { - latitude: number; - longitude: number; - }; + latitude: number + longitude: number + } export type MapViewDirectionsMode = - | "DRIVING" - | "BICYCLING" - | "TRANSIT" - | "WALKING"; + | 'DRIVING' + | 'BICYCLING' + | 'TRANSIT' + | 'WALKING' - export type MapViewDirectionsPrecision = - | "high" - | "low"; + export type MapViewDirectionsPrecision = 'high' | 'low' - export type MapViewDirectionsTimePrecision = - | "now" - | "none"; + export type MapViewDirectionsTimePrecision = 'now' | 'none' export interface MapViewDirectionsProps { /** * The origin location to start routing from. */ - origin?: MapViewDirectionsOrigin; + origin?: MapViewDirectionsOrigin /** * Array of waypoints to use between origin and destination. */ - waypoints?: MapViewDirectionsWaypoints[]; + waypoints?: MapViewDirectionsWaypoints[] /** * The destination location to start routing to. */ - destination?: MapViewDirectionsDestination; + destination?: MapViewDirectionsDestination /** * Your Google Maps API Key */ - apikey: string; + apikey: string /** * Callback that is called when the routing has started. */ - onStart?: (...args: any[]) => any; + onStart?: (...args: any[]) => any /** * Callback that is called when the routing has succesfully finished. */ - onReady?: (...args: MapDirectionsResponse[]) => any; + onReady?: (...args: any[]) => any /** * Callback that is called in case the routing has failed. */ - onError?: (...args: any[]) => any; + onError?: (...args: any[]) => any /** * Which transportation mode to use when calculating directions. * Allowed values are "DRIVING", "BICYCLING", "WALKING", and "TRANSIT". */ - mode?: MapViewDirectionsMode; + mode?: MapViewDirectionsMode /** * The precision to draw the polyline with. * Allowed values are "high", and "low". * Defaults to "low" */ - precision?: MapViewDirectionsPrecision; + precision?: MapViewDirectionsPrecision /** * The timePrecision to get Realtime traffic info. * Allowed values are "none", and "now". * Defaults to "none" */ - timePrecision?: MapViewDirectionsTimePrecision; + timePrecision?: MapViewDirectionsPrecision /** * If you include the channel parameter in your requests, * you can generate a Successful Requests report that shows a breakdown @@ -160,64 +92,64 @@ declare module "react-native-maps-directions" { * use the same client ID (such as externally facing access vs. internally * facing access). */ - channel?: string; + channel?: string /** * The language to use when calculating directions. */ - language?: string; + language?: string /** * Tweak if the rendered MapView. Polyline should reset or not * when calculating the route between origin and destionation. * Set to false if you see the directions line glitching. */ - resetOnChange?: boolean; + resetOnChange?: boolean /** * Set it to true if you would like Google Maps to re-order all the * waypoints to optimize the route for the fastest route. * Please be aware that if this option is enabled, * you will be billed for a higher rate by Google */ - optimizeWaypoints?: boolean; + optimizeWaypoints?: boolean /** * Directions API has a limit of 10 or 25 (depends on the billing plan) * waypoints per route. So waypoints array size is limited to those numbers. * Set this to true if you would like to automatically split waypoints to * multiple routes and by that avoid waypoints limit. */ - splitWaypoints?: boolean; + splitWaypoints?: boolean /** * Base URL of the Directions Service (API) you are using. * By default the Google Directions API is used * ("https://maps.googleapis.com/maps/api/directions/json"). * Usually you won't need to change this. */ - directionsServiceBaseUrl?: string; + directionsServiceBaseUrl?: string /** * If you are using strings for origin or destination, * sometimes you will get an incorrect route because * Google Maps API needs the region where this places belong to. */ - region?: string; + region?: string /** * @number * The stroke width to use for the path - the line displayed * by polyline between two navigation points. * Default: 1 */ - strokeWidth?: number; + strokeWidth?: number /** * @string * The stroke color to use for the path. * Default: "#000" */ - strokeColor?: string; + strokeColor?: string /** * @Array * The stroke colors to use for the path (iOS only). * Must be the same length as coordinates. * Default: null */ - strokeColors?: Array; + strokeColors?: Array /** * @string * The line cap style to apply to the open ends of the path. @@ -225,14 +157,14 @@ declare module "react-native-maps-directions" { * Note: lineCap is not yet supported for GoogleMaps provider on iOS. * Default: "round" */ - lineCap?: string; + lineCap?: string /** * @string * The line join style to apply to corners of the path. * Possible values are miter, round or bevel. * Default: "round" */ - lineJoin?: string; + lineJoin?: string /** * @number * The limiting value that helps avoid spikes at junctions @@ -243,7 +175,7 @@ declare module "react-native-maps-directions" { * to a bevel join. The default miter limit is 10, which results in the * conversion of miters whose angle at the joint is less than 11 degrees. */ - miterLimit?: number; + miterLimit?: number /** * @boolean * Boolean to indicate whether to draw each segment of the line as a geodesic @@ -251,7 +183,7 @@ declare module "react-native-maps-directions" { * shortest path between two points on the Earth's surface. * The geodesic curve is constructed assuming the Earth is a sphere. */ - geodesic?: boolean; + geodesic?: boolean /** * @number * (iOS only) The offset (in points) at which to start drawing the @@ -260,7 +192,7 @@ declare module "react-native-maps-directions" { * the patter 5-2-3-2 would cause drawing to begin in the middle of the first gap. * Default: 0 */ - lineDashPhase?: number; + lineDashPhase?: number /** * @Array * An array of numbers specifying the dash pattern to use for the path. @@ -270,18 +202,30 @@ declare module "react-native-maps-directions" { * followed by the second line segment length, and so on. * Default: null */ - lineDashPattern?: Array; + lineDashPattern?: Array /** * @boolean * Boolean to allow a polyline to be tappable and use the onPress function. */ - tappable?: boolean; + tappable?: boolean + /** + * Set it to true if you would like Google Maps to avoid tolls where possible. + */ + avoidTolls?: boolean + /** + * Set it to true if you would like Google Maps to avoid highways where possible. + */ + avoidHighways?: boolean + /** + * Set it to true if you would like Google Maps to avoid ferries where possible. + */ + avoidFerries?: boolean } export default class MapViewDirections extends React.Component< MapViewDirectionsProps, any > { - render(): JSX.Element; + render(): JSX.Element } } diff --git a/src/MapViewDirections.js b/src/MapViewDirections.js index 8e05906..09bf5be 100644 --- a/src/MapViewDirections.js +++ b/src/MapViewDirections.js @@ -1,357 +1,429 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { Polyline } from 'react-native-maps'; -import isEqual from 'lodash.isequal'; +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import MapView from 'react-native-maps' +import isEqual from 'lodash.isequal' -const WAYPOINT_LIMIT = 10; +const WAYPOINT_LIMIT = 10 class MapViewDirections extends Component { - - constructor(props) { - super(props); - - this.state = { - coordinates: null, - distance: null, - duration: null, - }; - } - - componentDidMount() { - this.fetchAndRenderRoute(this.props); - } - - componentDidUpdate(prevProps) { - if (!isEqual(prevProps.origin, this.props.origin) || !isEqual(prevProps.destination, this.props.destination) || !isEqual(prevProps.waypoints, this.props.waypoints) || !isEqual(prevProps.mode, this.props.mode) || !isEqual(prevProps.precision, this.props.precision) || !isEqual(prevProps.splitWaypoints, this.props.splitWaypoints)) { - if (this.props.resetOnChange === false) { - this.fetchAndRenderRoute(this.props); - } else { - this.resetState(() => { - this.fetchAndRenderRoute(this.props); - }); - } - } - } - - resetState = (cb = null) => { - this.setState({ - coordinates: null, - distance: null, - duration: null, - }, cb); - } - - decode(t) { - let points = []; - for (let step of t) { - let encoded = step.polyline.points; - let index = 0, len = encoded.length; - let lat = 0, lng = 0; - while (index < len) { - let b, shift = 0, result = 0; - do { - b = encoded.charAt(index++).charCodeAt(0) - 63; - result |= (b & 0x1f) << shift; - shift += 5; - } while (b >= 0x20); - - let dlat = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1)); - lat += dlat; - shift = 0; - result = 0; - do { - b = encoded.charAt(index++).charCodeAt(0) - 63; - result |= (b & 0x1f) << shift; - shift += 5; - } while (b >= 0x20); - let dlng = ((result & 1) != 0 ? ~(result >> 1) : (result >> 1)); - lng += dlng; - - points.push({ latitude: (lat / 1E5), longitude: (lng / 1E5) }); - } - } - return points; - } - - fetchAndRenderRoute = (props) => { - - let { - origin: initialOrigin, - destination: initialDestination, - waypoints: initialWaypoints = [], - apikey, - onStart, - onReady, - onError, - mode = 'DRIVING', - language = 'en', - optimizeWaypoints, - splitWaypoints, - directionsServiceBaseUrl = 'https://maps.googleapis.com/maps/api/directions/json', - region, - precision = 'low', - timePrecision = 'none', - channel, - } = props; - - if (!apikey) { - console.warn(`MapViewDirections Error: Missing API Key`); // eslint-disable-line no-console - return; - } - - if (!initialOrigin || !initialDestination) { - return; - } - - const timePrecisionString = timePrecision==='none' ? '' : timePrecision; - - // Routes array which we'll be filling. - // We'll perform a Directions API Request for reach route - const routes = []; - - // We need to split the waypoints in chunks, in order to not exceede the max waypoint limit - // ~> Chunk up the waypoints, yielding multiple routes - if (splitWaypoints && initialWaypoints && initialWaypoints.length > WAYPOINT_LIMIT) { - // Split up waypoints in chunks with chunksize WAYPOINT_LIMIT - const chunckedWaypoints = initialWaypoints.reduce((accumulator, waypoint, index) => { - const numChunk = Math.floor(index / WAYPOINT_LIMIT); - accumulator[numChunk] = [].concat((accumulator[numChunk] || []), waypoint); - return accumulator; - }, []); - - // Create routes for each chunk, using: - // - Endpoints of previous chunks as startpoints for the route (except for the first chunk, which uses initialOrigin) - // - Startpoints of next chunks as endpoints for the route (except for the last chunk, which uses initialDestination) - for (let i = 0; i < chunckedWaypoints.length; i++) { - routes.push({ - waypoints: chunckedWaypoints[i], - origin: (i === 0) ? initialOrigin : chunckedWaypoints[i-1][chunckedWaypoints[i-1].length - 1], - destination: (i === chunckedWaypoints.length - 1) ? initialDestination : chunckedWaypoints[i+1][0], - }); - } - } - - // No splitting of the waypoints is requested/needed. - // ~> Use one single route - else { - routes.push({ - waypoints: initialWaypoints, - origin: initialOrigin, - destination: initialDestination, - }); - } - - // Perform a Directions API Request for each route - Promise.all(routes.map((route, index) => { - let { - origin, - destination, - waypoints, - } = route; - - if (origin.latitude && origin.longitude) { - origin = `${origin.latitude},${origin.longitude}`; - } - - if (destination.latitude && destination.longitude) { - destination = `${destination.latitude},${destination.longitude}`; - } - - waypoints = waypoints - .map(waypoint => (waypoint.latitude && waypoint.longitude) ? `${waypoint.latitude},${waypoint.longitude}` : waypoint) - .join('|'); - - if (optimizeWaypoints) { - waypoints = `optimize:true|${waypoints}`; - } - - if (index === 0) { - onStart && onStart({ - origin, - destination, - waypoints: initialWaypoints, - }); - } - - return ( - this.fetchRoute(directionsServiceBaseUrl, origin, waypoints, destination, apikey, mode, language, region, precision, timePrecisionString, channel) - .then(result => { - return result; - }) - .catch(errorMessage => { - return Promise.reject(errorMessage); - }) - ); - })).then(results => { - // Combine all Directions API Request results into one - const result = results.reduce((acc, { distance, duration, coordinates, fare, legs, waypointOrder }) => { - acc.coordinates = [ - ...acc.coordinates, - ...coordinates, - ]; - acc.distance += distance; - acc.duration += duration; - acc.fares = [ - ...acc.fares, - fare, - ]; - acc.legs = legs; - acc.waypointOrder = [ - ...acc.waypointOrder, - waypointOrder, - ]; - - return acc; - }, { - coordinates: [], - distance: 0, - duration: 0, - fares: [], - legs: [], - waypointOrder: [], - }); - - // Plot it out and call the onReady callback - this.setState({ - coordinates: result.coordinates, - }, function() { - if (onReady) { - onReady(result); - } - }); - }) - .catch(errorMessage => { - this.resetState(); - console.warn(`MapViewDirections Error: ${errorMessage}`); // eslint-disable-line no-console - onError && onError(errorMessage); - }); - } - - fetchRoute(directionsServiceBaseUrl, origin, waypoints, destination, apikey, mode, language, region, precision, timePrecision, channel) { - - // Define the URL to call. Only add default parameters to the URL if it's a string. - let url = directionsServiceBaseUrl; - if (typeof (directionsServiceBaseUrl) === 'string') { - url += `?origin=${origin}&waypoints=${waypoints}&destination=${destination}&key=${apikey}&mode=${mode.toLowerCase()}&language=${language}®ion=${region}`; - if(timePrecision){ - url+=`&departure_time=${timePrecision}`; - } - if(channel){ - url+=`&channel=${channel}`; - } - } - - return fetch(url) - .then(response => response.json()) - .then(json => { - - if (json.status !== 'OK') { - const errorMessage = json.error_message || json.status || 'Unknown error'; - return Promise.reject(errorMessage); - } - - if (json.routes.length) { - - const route = json.routes[0]; - - return Promise.resolve({ - distance: route.legs.reduce((carry, curr) => { - return carry + curr.distance.value; - }, 0) / 1000, - duration: route.legs.reduce((carry, curr) => { - return carry + (curr.duration_in_traffic ? curr.duration_in_traffic.value : curr.duration.value); - }, 0) / 60, - coordinates: ( - (precision === 'low') ? - this.decode([{polyline: route.overview_polyline}]) : - route.legs.reduce((carry, curr) => { - return [ - ...carry, - ...this.decode(curr.steps), - ]; - }, []) - ), - fare: route.fare, - waypointOrder: route.waypoint_order, - legs: route.legs, - }); - - } else { - return Promise.reject(); - } - }) - .catch(err => { - return Promise.reject(`Error on GMAPS route request: ${err}`); - }); - } - - render() { - const { coordinates } = this.state; - - if (!coordinates) { - return null; - } - - const { - origin, // eslint-disable-line no-unused-vars - waypoints, // eslint-disable-line no-unused-vars - splitWaypoints, // eslint-disable-line no-unused-vars - destination, // eslint-disable-line no-unused-vars - apikey, // eslint-disable-line no-unused-vars - onReady, // eslint-disable-line no-unused-vars - onError, // eslint-disable-line no-unused-vars - mode, // eslint-disable-line no-unused-vars - language, // eslint-disable-line no-unused-vars - region, // eslint-disable-line no-unused-vars - precision, // eslint-disable-line no-unused-vars - ...props - } = this.props; - - return ( - - ); - } - + constructor(props) { + super(props) + + this.state = { + coordinates: null, + distance: null, + duration: null, + } + } + + componentDidMount() { + this.fetchAndRenderRoute(this.props) + } + + componentDidUpdate(prevProps) { + if ( + !isEqual(prevProps.origin, this.props.origin) || + !isEqual(prevProps.destination, this.props.destination) || + !isEqual(prevProps.waypoints, this.props.waypoints) || + !isEqual(prevProps.mode, this.props.mode) || + !isEqual(prevProps.precision, this.props.precision) || + !isEqual(prevProps.splitWaypoints, this.props.splitWaypoints) || + !isEqual(prevProps.avoidTolls, this.props.avoidTolls) || + !isEqual(prevProps.avoidHighways, this.props.avoidHighways) || + !isEqual(prevProps.avoidFerries, this.props.avoidFerries) + ) { + if (this.props.resetOnChange === false) { + this.fetchAndRenderRoute(this.props) + } else { + this.resetState(() => { + this.fetchAndRenderRoute(this.props) + }) + } + } + } + + resetState = (cb = null) => { + this.setState( + { + coordinates: null, + distance: null, + duration: null, + }, + cb + ) + } + + decode(t) { + let points = [] + for (let step of t) { + let encoded = step.polyline.points + let index = 0, + len = encoded.length + let lat = 0, + lng = 0 + while (index < len) { + let b, + shift = 0, + result = 0 + do { + b = encoded.charAt(index++).charCodeAt(0) - 63 + result |= (b & 0x1f) << shift + shift += 5 + } while (b >= 0x20) + + let dlat = (result & 1) != 0 ? ~(result >> 1) : result >> 1 + lat += dlat + shift = 0 + result = 0 + do { + b = encoded.charAt(index++).charCodeAt(0) - 63 + result |= (b & 0x1f) << shift + shift += 5 + } while (b >= 0x20) + let dlng = (result & 1) != 0 ? ~(result >> 1) : result >> 1 + lng += dlng + + points.push({ latitude: lat / 1e5, longitude: lng / 1e5 }) + } + } + return points + } + + fetchAndRenderRoute = (props) => { + let { + origin: initialOrigin, + destination: initialDestination, + waypoints: initialWaypoints = [], + apikey, + onStart, + onReady, + onError, + mode = 'DRIVING', + language = 'en', + optimizeWaypoints, + splitWaypoints, + directionsServiceBaseUrl = 'https://maps.googleapis.com/maps/api/directions/json', + region, + precision = 'low', + timePrecision = 'none', + channel, + avoidTolls, + avoidHighways, + avoidFerries, + } = props + + if (!apikey) { + console.warn(`MapViewDirections Error: Missing API Key`) // eslint-disable-line no-console + return + } + + if (!initialOrigin || !initialDestination) { + return + } + + const timePrecisionString = timePrecision === 'none' ? '' : timePrecision + + // Routes array which we'll be filling. + // We'll perform a Directions API Request for reach route + const routes = [] + + // We need to split the waypoints in chunks, in order to not exceede the max waypoint limit + // ~> Chunk up the waypoints, yielding multiple routes + if ( + splitWaypoints && + initialWaypoints && + initialWaypoints.length > WAYPOINT_LIMIT + ) { + // Split up waypoints in chunks with chunksize WAYPOINT_LIMIT + const chunckedWaypoints = initialWaypoints.reduce( + (accumulator, waypoint, index) => { + const numChunk = Math.floor(index / WAYPOINT_LIMIT) + accumulator[numChunk] = [].concat( + accumulator[numChunk] || [], + waypoint + ) + return accumulator + }, + [] + ) + + // Create routes for each chunk, using: + // - Endpoints of previous chunks as startpoints for the route (except for the first chunk, which uses initialOrigin) + // - Startpoints of next chunks as endpoints for the route (except for the last chunk, which uses initialDestination) + for (let i = 0; i < chunckedWaypoints.length; i++) { + routes.push({ + waypoints: chunckedWaypoints[i], + origin: + i === 0 + ? initialOrigin + : chunckedWaypoints[i - 1][chunckedWaypoints[i - 1].length - 1], + destination: + i === chunckedWaypoints.length - 1 + ? initialDestination + : chunckedWaypoints[i + 1][0], + }) + } + } + + // No splitting of the waypoints is requested/needed. + // ~> Use one single route + else { + routes.push({ + waypoints: initialWaypoints, + origin: initialOrigin, + destination: initialDestination, + }) + } + + // Perform a Directions API Request for each route + Promise.all( + routes.map((route, index) => { + let { origin, destination, waypoints } = route + + if (origin.latitude && origin.longitude) { + origin = `${origin.latitude},${origin.longitude}` + } + + if (destination.latitude && destination.longitude) { + destination = `${destination.latitude},${destination.longitude}` + } + + waypoints = waypoints + .map((waypoint) => + waypoint.latitude && waypoint.longitude + ? `${waypoint.latitude},${waypoint.longitude}` + : waypoint + ) + .join('|') + + if (optimizeWaypoints) { + waypoints = `optimize:true|${waypoints}` + } + + if (index === 0) { + onStart && + onStart({ + origin, + destination, + waypoints: initialWaypoints, + }) + } + + return this.fetchRoute( + directionsServiceBaseUrl, + origin, + waypoints, + destination, + apikey, + mode, + language, + region, + precision, + timePrecisionString, + channel, + avoidTolls, + avoidFerries, + avoidHighways + ) + .then((result) => { + return result + }) + .catch((errorMessage) => { + return Promise.reject(errorMessage) + }) + }) + ) + .then((results) => { + // Combine all Directions API Request results into one + const result = results.reduce( + (acc, { distance, duration, coordinates, fare, waypointOrder }) => { + acc.coordinates = [...acc.coordinates, ...coordinates] + acc.distance += distance + acc.duration += duration + acc.fares = [...acc.fares, fare] + acc.waypointOrder = [...acc.waypointOrder, waypointOrder] + + return acc + }, + { + coordinates: [], + distance: 0, + duration: 0, + fares: [], + waypointOrder: [], + } + ) + + // Plot it out and call the onReady callback + this.setState( + { + coordinates: result.coordinates, + }, + function () { + if (onReady) { + onReady(result) + } + } + ) + }) + .catch((errorMessage) => { + this.resetState() + console.warn(`MapViewDirections Error: ${errorMessage}`) // eslint-disable-line no-console + onError && onError(errorMessage) + }) + } + + fetchRoute( + directionsServiceBaseUrl, + origin, + waypoints, + destination, + apikey, + mode, + language, + region, + precision, + timePrecision, + channel, + avoidTolls, + avoidFerries, + avoidHighways + ) { + // Define the URL to call. Only add default parameters to the URL if it's a string. + let url = directionsServiceBaseUrl + if (typeof directionsServiceBaseUrl === 'string') { + url += `?origin=${origin}&waypoints=${waypoints}&destination=${destination}&key=${apikey}&mode=${mode.toLowerCase()}&language=${language}®ion=${region}` + if (timePrecision) { + url += `&departure_time=${timePrecision}` + } + if (channel) { + url += `&channel=${channel}` + } + if (avoidTolls) { + url += `&avoid=tolls` + } + if (avoidHighways) { + url += `&avoid=highways` + } + if (avoidFerries) { + url += `&avoid=ferries` + } + } + + return fetch(url) + .then((response) => response.json()) + .then((json) => { + if (json.status !== 'OK') { + const errorMessage = + json.error_message || json.status || 'Unknown error' + return Promise.reject(errorMessage) + } + + if (json.routes.length) { + const route = json.routes[0] + + return Promise.resolve({ + distance: + route.legs.reduce((carry, curr) => { + return carry + curr.distance.value + }, 0) / 1000, + duration: + route.legs.reduce((carry, curr) => { + return ( + carry + + (curr.duration_in_traffic + ? curr.duration_in_traffic.value + : curr.duration.value) + ) + }, 0) / 60, + coordinates: + precision === 'low' + ? this.decode([{ polyline: route.overview_polyline }]) + : route.legs.reduce((carry, curr) => { + return [...carry, ...this.decode(curr.steps)] + }, []), + fare: route.fare, + waypointOrder: route.waypoint_order, + }) + } else { + return Promise.reject() + } + }) + .catch((err) => { + return Promise.reject(`Error on GMAPS route request: ${err}`) + }) + } + + render() { + const { coordinates } = this.state + + if (!coordinates) { + return null + } + + const { + origin, // eslint-disable-line no-unused-vars + waypoints, // eslint-disable-line no-unused-vars + splitWaypoints, // eslint-disable-line no-unused-vars + destination, // eslint-disable-line no-unused-vars + apikey, // eslint-disable-line no-unused-vars + onReady, // eslint-disable-line no-unused-vars + onError, // eslint-disable-line no-unused-vars + mode, // eslint-disable-line no-unused-vars + language, // eslint-disable-line no-unused-vars + region, // eslint-disable-line no-unused-vars + precision, // eslint-disable-line no-unused-vars + avoidFerries, // eslint-disable-line no-unused-vars + avoidHighways, // eslint-disable-line no-unused-vars + avoidTolls, // eslint-disable-line no-unused-vars + ...props + } = this.props + + return + } } MapViewDirections.propTypes = { - origin: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.shape({ - latitude: PropTypes.number.isRequired, - longitude: PropTypes.number.isRequired, - }), - ]), - waypoints: PropTypes.arrayOf( - PropTypes.oneOfType([ - PropTypes.string, - PropTypes.shape({ - latitude: PropTypes.number.isRequired, - longitude: PropTypes.number.isRequired, - }), - ]), - ), - destination: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.shape({ - latitude: PropTypes.number.isRequired, - longitude: PropTypes.number.isRequired, - }), - ]), - apikey: PropTypes.string.isRequired, - onStart: PropTypes.func, - onReady: PropTypes.func, - onError: PropTypes.func, - mode: PropTypes.oneOf(['DRIVING', 'BICYCLING', 'TRANSIT', 'WALKING']), - language: PropTypes.string, - resetOnChange: PropTypes.bool, - optimizeWaypoints: PropTypes.bool, - splitWaypoints: PropTypes.bool, - directionsServiceBaseUrl: PropTypes.string, - region: PropTypes.string, - precision: PropTypes.oneOf(['high', 'low']), - timePrecision: PropTypes.oneOf(['now', 'none']), - channel: PropTypes.string, -}; - -export default MapViewDirections; + origin: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.shape({ + latitude: PropTypes.number.isRequired, + longitude: PropTypes.number.isRequired, + }), + ]), + waypoints: PropTypes.arrayOf( + PropTypes.oneOfType([ + PropTypes.string, + PropTypes.shape({ + latitude: PropTypes.number.isRequired, + longitude: PropTypes.number.isRequired, + }), + ]) + ), + destination: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.shape({ + latitude: PropTypes.number.isRequired, + longitude: PropTypes.number.isRequired, + }), + ]), + apikey: PropTypes.string.isRequired, + onStart: PropTypes.func, + onReady: PropTypes.func, + onError: PropTypes.func, + mode: PropTypes.oneOf(['DRIVING', 'BICYCLING', 'TRANSIT', 'WALKING']), + language: PropTypes.string, + resetOnChange: PropTypes.bool, + optimizeWaypoints: PropTypes.bool, + splitWaypoints: PropTypes.bool, + directionsServiceBaseUrl: PropTypes.string, + region: PropTypes.string, + precision: PropTypes.oneOf(['high', 'low']), + timePrecision: PropTypes.oneOf(['now', 'none']), + channel: PropTypes.string, + avoidFerries: PropTypes.bool, + avoidTolls: PropTypes.bool, + avoidHighways: PropTypes.bool, +} + +export default MapViewDirections