diff --git a/README.md b/README.md index d80d068..d2d0b3c 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,7 @@ Once the directions in between `destination` and `origin` has been fetched, a `M | `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). +| `isMemoized` | `boolean` or `Function` | `null` | If you want to memoize requests to google API in order to reduce cost, you can either pass true which will memoize your requests automatically by the function signature, alternativly you can use a callback to decide when to memoize your requests based on origin, destination and cachedResult. #### 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. @@ -214,6 +215,11 @@ class Example extends Component { onError={(errorMessage) => { // console.log('GOT AN ERROR'); }} + // By default all requests are not memoized, you can pass either a boolean here or a resolver function to decide when to memoize the request + isMemoized={({ origin, destination, cachedResults, /* Rest of the props supplied to the component when request was made */ }) => { + // Logic to decide when to memoize goes here + return false + }} /> )} @@ -224,6 +230,7 @@ class Example extends Component { export default Example; ``` + ## Example App An example app can be found in a separate repo, located at [https://github.com/bramus/react-native-maps-directions-example](https://github.com/bramus/react-native-maps-directions-example). diff --git a/index.d.ts b/index.d.ts index 58c1a7d..ede6e5c 100644 --- a/index.d.ts +++ b/index.d.ts @@ -89,6 +89,12 @@ declare module "react-native-maps-directions" { * Defaults to "none" */ timePrecision?: MapViewDirectionsTimePrecision; + /** + * If you pass true, the requests will be cached by the origin, destination, region, and all the provided options to the component + * you can also pass a function that returns a boolean, + * that function receives callback with all the fields that was used in order to fetch the directions + */ + isMemoized?: Function | boolean; /** * If you include the channel parameter in your requests, * you can generate a Successful Requests report that shows a breakdown diff --git a/src/MapViewDirections.js b/src/MapViewDirections.js index ac08d73..1f92402 100644 --- a/src/MapViewDirections.js +++ b/src/MapViewDirections.js @@ -5,6 +5,30 @@ import isEqual from 'lodash.isequal'; const WAYPOINT_LIMIT = 10; +const promiseMemoize = (fn, resolver) => { + let cache = {}; + return (...args) => { + let strX = JSON.stringify(args); + const trySetResultsToCache = () => { + return (cache[strX] = fn(...args).catch((x) => { + delete cache[strX]; + return Promise.reject(x); + })); + }; + + if (strX in cache) { + return Promise.resolve(cache[strX]).then(cachedResult => { + if (resolver && !resolver({ cachedResult, providedArgs: args[0] })) { + return trySetResultsToCache(); + } + return cachedResult; + }); + } else { + return trySetResultsToCache(); + } + }; +}; + class MapViewDirections extends Component { constructor(props) { @@ -103,8 +127,8 @@ class MapViewDirections extends Component { return; } - const timePrecisionString = timePrecision==='none' ? '' : timePrecision; - + const timePrecisionString = timePrecision === 'none' ? '' : timePrecision; + // Routes array which we'll be filling. // We'll perform a Directions API Request for reach route const routes = []; @@ -114,8 +138,8 @@ class MapViewDirections extends Component { 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); + const numChunk = Math.floor(index / WAYPOINT_LIMIT); + accumulator[numChunk] = [].concat((accumulator[numChunk] || []), waypoint); return accumulator; }, []); @@ -125,12 +149,12 @@ class MapViewDirections extends Component { 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], + 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 { @@ -174,7 +198,7 @@ class MapViewDirections extends Component { } return ( - this.fetchRoute(directionsServiceBaseUrl, origin, waypoints, destination, apikey, mode, language, region, precision, timePrecisionString, channel) + this.fetchRoute({ directionsServiceBaseUrl, origin, waypoints, destination, apikey, mode, language, region, precision, timePrecision: timePrecisionString, channel }) .then(result => { return result; }) @@ -212,7 +236,7 @@ class MapViewDirections extends Component { // Plot it out and call the onReady callback this.setState({ coordinates: result.coordinates, - }, function() { + }, function () { if (onReady) { onReady(result); } @@ -225,17 +249,16 @@ class MapViewDirections extends Component { }); } - fetchRoute(directionsServiceBaseUrl, origin, waypoints, destination, apikey, mode, language, region, precision, timePrecision, channel) { - + fetchRoute = promiseMemoize(({ 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 (timePrecision) { + url += `&departure_time=${timePrecision}`; } - if(channel){ - url+=`&channel=${channel}`; + if (channel) { + url += `&channel=${channel}`; } } @@ -261,7 +284,7 @@ class MapViewDirections extends Component { }, 0) / 60, coordinates: ( (precision === 'low') ? - this.decode([{polyline: route.overview_polyline}]) : + this.decode([{ polyline: route.overview_polyline }]) : route.legs.reduce((carry, curr) => { return [ ...carry, @@ -280,9 +303,27 @@ class MapViewDirections extends Component { .catch(err => { return Promise.reject(`Error on GMAPS route request: ${err}`); }); - } + }, ({ cachedResult, providedArgs }) => { + const { isMemoized } = this.props; + + + if (typeof isMemoized === "boolean") { + return isMemoized; + } + + if (!isMemoized || (typeof isMemoized !== 'function')) { + return false; + } + + try { + return isMemoized({ cachedResult, ...providedArgs }); + } catch { + return false; + } + }) render() { + const { coordinates } = this.state; if (!coordinates) { @@ -349,6 +390,7 @@ MapViewDirections.propTypes = { precision: PropTypes.oneOf(['high', 'low']), timePrecision: PropTypes.oneOf(['now', 'none']), channel: PropTypes.string, + isMemoized: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]), }; export default MapViewDirections;