From 0d90fbf26eb588fffb13a6b51c5b3a5b8a173218 Mon Sep 17 00:00:00 2001
From: Teddy Wing
Date: Thu, 15 Mar 2018 16:19:24 +0100
Subject: JourneyPatternsCollection#show: Fallback to route costs
When editing a `JourneyPattern`, you can edit the distance & time costs
between stops. We want to pre-fill these cost values (in the input
fields) if they haven't already been set by a user. This way, they get
an existing estimate of the cost and don't have to enter a value
manually unless the default doesn't work.
The pre-filled values come from `Route#costs`, which get calculated
ahead of time via the TomTom API.
Add a new `fetchRouteCosts` function that will fetch the costs for the
current route from the API. This function also caches the value on
`actions` so we don't keep making requests since the data isn't going to
change. Put the cached fetch in a `requestAnimationFrame` as a sort of
timeout to prevent a warning from React complaining about doing this
during a `render()` call.
Update `getTimeAndDistanceBetweenStops` to use the cost value from the
route costs instead of the journey pattern costs if it doesn't exist.
The `totalDistance` and `totalTime` we moved into `componentWillUpdate`
instead of `render()` because we thought that might be the cause of the
`render()` warning I mentioned above. Decided to leave this part in even
though it doesn't have anything to do with the goal of the changes here,
because it seemed like an okay change.
The `RECEIVE_ROUTE_COSTS` reducer will update the state with route
costs. We need the default distance:0 time:0 to avoid infinitely
fetching the costs API if a cost with the given key can't be found.
Put the route cost API URL in `window.routeCostsUrl` to allow us to get
it from the Rails URL helper.
Huge thanks to Johan for pairing with me on this code and walking me
through the setup for integrating the route costs JSON response into the
frontend interface.
Refs #6203
---
 app/javascript/journey_patterns/actions/index.js   | 30 ++++++++++
 .../journey_patterns/components/JourneyPattern.js  | 26 +++++++--
 .../journey_patterns/components/JourneyPatterns.js |  4 +-
 .../containers/JourneyPatternList.js               |  3 +
 .../journey_patterns/reducers/journeyPatterns.js   | 11 ++++
 .../journey_patterns_collections/show.html.slim    |  1 +
 .../components/JourneyPattern_spec.js              |  4 +-
 .../components/JourneyPatterns_spec.js             |  4 +-
 .../reducers/journey_patterns_spec.js              | 64 ++++++++++++++++++++++
 9 files changed, 139 insertions(+), 8 deletions(-)
diff --git a/app/javascript/journey_patterns/actions/index.js b/app/javascript/journey_patterns/actions/index.js
index a70a2e6f2..b90908264 100644
--- a/app/javascript/journey_patterns/actions/index.js
+++ b/app/javascript/journey_patterns/actions/index.js
@@ -20,6 +20,12 @@ const actions = {
     type: "RECEIVE_ERRORS",
     json
   }),
+  receiveRouteCosts: (costs, key, index) => ({
+    type: "RECEIVE_ROUTE_COSTS",
+    costs,
+    key,
+    index
+  }),
   unavailableServer : () => ({
     type: 'UNAVAILABLE_SERVER'
   }),
@@ -213,6 +219,30 @@ const actions = {
         }
       })
   },
+  fetchRouteCosts: (dispatch, key, index) => {
+    if (actions.routeCostsCache) {
+      // Dispatch asynchronously to prevent warning when
+      // this executes during `render()`
+      requestAnimationFrame(() => {
+        dispatch(actions.receiveRouteCosts(
+          actions.routeCostsCache,
+          key,
+          index
+        ))
+      })
+    } else {
+      fetch(window.routeCostsUrl, {
+        credentials: 'same-origin',
+      }).then(response => {
+        return response.json()
+      }).then(json => {
+        let costs = json.costs ? json.costs : {}
+        actions.routeCostsCache = costs
+
+        dispatch(actions.receiveRouteCosts(costs, key, index))
+      })
+    }
+  },
   getChecked : (jp) => {
     return jp.filter((obj) => {
       return obj.checked
diff --git a/app/javascript/journey_patterns/components/JourneyPattern.js b/app/javascript/journey_patterns/components/JourneyPattern.js
index d20294594..15d8b6db4 100644
--- a/app/javascript/journey_patterns/components/JourneyPattern.js
+++ b/app/javascript/journey_patterns/components/JourneyPattern.js
@@ -98,12 +98,24 @@ export default class JourneyPattern extends Component{
 
   getTimeAndDistanceBetweenStops(from, to){
     let costsKey = from + "-" + to
-    let costs = this.props.value.costs[costsKey] || {distance: 0, time: 0}
+    let costs = this.getCosts(costsKey)
     let time = costs['time'] || 0
     let distance = costs['distance'] || 0
     return [costsKey, costs, time, distance]
   }
 
+  getCosts(costsKey) {
+    let cost = this.props.value.costs[costsKey]
+
+    if (cost) {
+      return cost
+    }
+
+    this.props.fetchRouteCosts(costsKey)
+
+    return { distance: 0, time: 0 }
+  }
+
   formatDistance(distance){
     return parseFloat(Math.round(distance * 100) / 100).toFixed(2) + " km"
   }
@@ -119,9 +131,12 @@ export default class JourneyPattern extends Component{
     }
   }
 
+  componentWillUpdate() {
+    [this.totalTime, this.totalDistance] = this.totals(false)
+  }
+
   render() {
     this.previousSpId = undefined
-    let [totalTime, totalDistance] = this.totals(false)
     let [commercialTotalTime, commercialTotalDistance] = this.totals(true)
     return (
       
@@ -131,8 +146,8 @@ export default class JourneyPattern extends Component{
           
{actions.getChecked(this.props.value.stop_points).length} arrĂȘt(s)
           {this.hasFeature('costs_in_journey_patterns') &&
             
-              {totalDistance}
-              {totalTime}
+              {this.totalDistance}
+              {this.totalTime}
             
           }
           {this.hasFeature('costs_in_journey_patterns') &&
@@ -228,5 +243,6 @@ JourneyPattern.propTypes = {
   onCheckboxChange: PropTypes.func.isRequired,
   onOpenEditModal: PropTypes.func.isRequired,
   onDeleteJourneyPattern: PropTypes.func.isRequired,
-  journeyPatterns: PropTypes.object.isRequired
+  journeyPatterns: PropTypes.object.isRequired,
+  fetchRouteCosts: PropTypes.func.isRequired
 }
diff --git a/app/javascript/journey_patterns/components/JourneyPatterns.js b/app/javascript/journey_patterns/components/JourneyPatterns.js
index e8b6bf143..930acb390 100644
--- a/app/javascript/journey_patterns/components/JourneyPatterns.js
+++ b/app/javascript/journey_patterns/components/JourneyPatterns.js
@@ -138,6 +138,7 @@ export default class JourneyPatterns extends Component {
                       status= {this.props.status}
                       editMode= {this.props.editMode}
                       journeyPatterns= {this}
+                      fetchRouteCosts={(costsKey) => this.props.fetchRouteCosts(costsKey, index)}
                       />
                   )}