diff --git a/src/MapViewDirections.js b/src/MapViewDirections.js index 8e05906..7b3d3fe 100644 --- a/src/MapViewDirections.js +++ b/src/MapViewDirections.js @@ -1,357 +1,415 @@ -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 { Polyline } from "react-native-maps"; +import isEqual from "lodash.isequal"; 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) + ) { + 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 = [ + ...acc.legs, + ...legs, // Combine legs arrays from all split routes + ]; + acc.waypointOrder = [ + ...acc.waypointOrder, + ...waypointOrder, // Combine waypointOrder arrays + ]; + + return acc; + }, + { + coordinates: [], + distance: 0, + duration: 0, + fares: [], + legs: [], // Initialize legs as an empty array + 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 ; + } } 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, + 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;