From 9c08ac6db4e4206ba8589109b5780ff649c58b74 Mon Sep 17 00:00:00 2001 From: Or Schneider Date: Thu, 15 Jul 2021 18:04:57 +0300 Subject: [PATCH 1/4] allow memoize responses --- README.md | 7 ++++ src/MapViewDirections.js | 75 +++++++++++++++++++++++++++++++--------- 2 files changed, 65 insertions(+), 17 deletions(-) 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/src/MapViewDirections.js b/src/MapViewDirections.js index ac08d73..baf486e 100644 --- a/src/MapViewDirections.js +++ b/src/MapViewDirections.js @@ -5,6 +5,29 @@ import isEqual from 'lodash.isequal'; const WAYPOINT_LIMIT = 10; +const promiseMemoize = (fn, resolver) => { + let cache = {}; + return (...args) => { + let strX = JSON.stringify(args); + if (strX in cache) { + return Promise.resolve(cache[strX]).then(cachedResult=> { + if (resolver && !resolver({ cachedResult, providedArgs: args[0] })) { + return (cache[strX] = fn(...args).catch((x) => { + delete cache[strX]; + return x; + })); + } + return cache[strX]; + }); + } else { + return (cache[strX] = fn(...args).catch((x) => { + delete cache[strX]; + return x; + })); + } + }; +}; + class MapViewDirections extends Component { constructor(props) { @@ -103,8 +126,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 +137,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 +148,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 +197,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 +235,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 +248,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 +283,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 +302,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 +389,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; From 9af7556bd7b15c0b65c8694f928c6349e889d5d7 Mon Sep 17 00:00:00 2001 From: Or Schneider Date: Thu, 15 Jul 2021 18:35:31 +0300 Subject: [PATCH 2/4] dts --- index.d.ts | 6 ++++++ 1 file changed, 6 insertions(+) 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 From 697266ab775f48720b8fedf1c8965cda80cec333 Mon Sep 17 00:00:00 2001 From: Or Schneider Date: Thu, 15 Jul 2021 18:37:33 +0300 Subject: [PATCH 3/4] clean ups --- src/MapViewDirections.js | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/MapViewDirections.js b/src/MapViewDirections.js index baf486e..3ba49ad 100644 --- a/src/MapViewDirections.js +++ b/src/MapViewDirections.js @@ -8,22 +8,23 @@ const WAYPOINT_LIMIT = 10; const promiseMemoize = (fn, resolver) => { let cache = {}; return (...args) => { + const trySetResultsToCache = () => { + return (cache[strX] = fn(...args).catch((x) => { + delete cache[strX]; + return Promise.reject(x); + })); + }; + let strX = JSON.stringify(args); if (strX in cache) { - return Promise.resolve(cache[strX]).then(cachedResult=> { + return Promise.resolve(cache[strX]).then(cachedResult => { if (resolver && !resolver({ cachedResult, providedArgs: args[0] })) { - return (cache[strX] = fn(...args).catch((x) => { - delete cache[strX]; - return x; - })); + return trySetResultsToCache(); } - return cache[strX]; + return cachedResult; }); } else { - return (cache[strX] = fn(...args).catch((x) => { - delete cache[strX]; - return x; - })); + return trySetResultsToCache(); } }; }; @@ -305,7 +306,7 @@ class MapViewDirections extends Component { }, ({ cachedResult, providedArgs }) => { const { isMemoized } = this.props; - + if (typeof isMemoized === "boolean") { return isMemoized; } From 1bae708adc0e6f6cf6ef02bedfb5c5cb78b3646d Mon Sep 17 00:00:00 2001 From: Or Schneider Date: Thu, 15 Jul 2021 18:41:39 +0300 Subject: [PATCH 4/4] small fix --- src/MapViewDirections.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/MapViewDirections.js b/src/MapViewDirections.js index 3ba49ad..1f92402 100644 --- a/src/MapViewDirections.js +++ b/src/MapViewDirections.js @@ -8,14 +8,14 @@ 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); })); }; - - let strX = JSON.stringify(args); + if (strX in cache) { return Promise.resolve(cache[strX]).then(cachedResult => { if (resolver && !resolver({ cachedResult, providedArgs: args[0] })) {