Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add memoization option to avoid unnecessary requests #173

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
}}
/>
)}
</MapView>
Expand All @@ -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).
Expand Down
6 changes: 6 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
76 changes: 59 additions & 17 deletions src/MapViewDirections.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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 = [];
Expand All @@ -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;
}, []);

Expand All @@ -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 {
Expand Down Expand Up @@ -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;
})
Expand Down Expand Up @@ -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);
}
Expand All @@ -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}&region=${region}`;
if(timePrecision){
url+=`&departure_time=${timePrecision}`;
if (timePrecision) {
url += `&departure_time=${timePrecision}`;
}
if(channel){
url+=`&channel=${channel}`;
if (channel) {
url += `&channel=${channel}`;
}
}

Expand All @@ -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,
Expand All @@ -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) {
Expand Down Expand Up @@ -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;