Last updated on
Rain Detection in Trip Planner App
- Points
- 80 pts
- Topics
- Concurrency, Futures, List Comprehension
- Files to Submit
src/main/scala/weather/WeatherService.scalasrc/main/scala/rainapp/RainApp.scala
Introduction
Let’s build a trip planner app!
One of the stand-out features of our app will be the ability to detect rain in a hiking trip: the user inputs the two endpoints of their route, and we build a route and compute whether any points on the route might have unsafe rain.
The infrastructure of the app is presented by the following diagram:
In particular,
- The weather stations send rain measurement data at their locations to the weather service, which will be stored in the database.
- The weather service will query a rain forecaster to predict the rain amount at a given location, from the stored measurements.
- The pathfinder service takes two endpoints and plans a route between the endpoints, returning a list of pairs where each pair contains a location and the duration to that location from the start of the hike.
- And finally, our trip planner app, that will utilize both of the above services to warn the user if their planned trip might end up in unsafe rain.
The pathfinder service has been provided to us, and our job is to implement the weather service and the trip planner app.
You can implement the weather service and the trip planner app independently: most tests for a given task do not require other tasks to be implemented.
The set of tests utilizing all implementations are marked as
integration:, and will be worth 16 points.
In this mini-lab, we use the Instant type to represent instants in time,
and the Duration type to represent time intervals.
How to instantiate a duration with a given amount of hours?
You can use the Duration.ofHours function, which takes an Int and returns a Duration:
val tenHours: Duration = Duration.ofHours(10)
How to calculate the duration between two time instants?
You can call the Duration.between function, which takes two Instants and returns the duration between them:
val durationFromStartToEnd: Duration = Duration.between(startInstant, endInstant)
For example, if startInstant represents 00:00:00 Jan 1st, and endInstant represents 10:00:00 Jan 1st, then durationFromStartToEnd will represents 10 hours.
How to compare two durations?
You can compare two Durations using the compareTo method, which returns an Int:
if duration0.compareTo(duration1) < 0 then
// duration0 is less than duration1
else if duration0.compareTo(duration1) == 0 then
// duration0 is equal to duration1
else
// duration0 is greater than duration1
How to compare two instants?
You can compare two Instants using the compareTo method, which returns an Int:
if instant0.compareTo(instant1) < 0 then
// instant0 is less than instant1
else if instant0.compareTo(instant1) == 0 then
// instant0 is equal to instant1
else
// instant0 is greater than instant1
How to add a specific amount of time to an instant?
You can add a Duration to an Instant using Instant.plus, which returns a new Instant:
val instant1 = instant0.plus(Duration.ofHours(10))
For example, if instant0 represents 00:00:00 Jan 1st, then instant1 will represent 10:00:00 Jan 1st.
Implementing the weather service
Your solutions must be asynchronous and non-blocking. In particular, you are forbidden from using Await in your solutions.
The weather service (implemented as WeatherService class in Scala) has two
responsibilities:
-
Receive the measurement data from weather stations and store them into the
Database./** A rain measurement from the station, at the given time and amount (in * millimeters). */ case class Measurement(stationId: Int, when: Instant, rainAmount: Double)rainapp/./src/main/scala/weather/Database.scala
Handling a received measurement is done in the method
handleReceivedMeasurement. -
Perform rain prediction by calling the
computeForecastmethod of theRainForecasterwith the desired location, time, and a set of relevant measurements queried from theDatabase.Performing rain prediction is done in the method
predictPrecipitation.
Familiarize yourself with the Database interface: it stores a list of
weather Stations, and allows you to query/insert/remove rain
measurements.
💪 Task: Implement handleReceivedMeasurement (16 pts)
Your task is to implement handleReceivedMeasurement, which given the data of
a rain measurement from a station, stores it in the database and performs
cleanup of old measurements.
/** Handles a measurement from a weather station. */
def handleReceivedMeasurement(
stationId: Int,
when: Instant,
rainAmount: Double
): Future[Unit] =
???
rainapp/./src/main/scala/weather/WeatherService.scala
The function should guarantee that after the Future completes:
- The measurement is stored in the database, and
- As an additional effect, all measurements from the same station older than 24 hours (exactly 24 hours is not included) from the submitted measurement are removed.
You can run tests for this task using the testOnly command.
sbt:rainapp> testOnly -- "*handleReceivedMeasurement*"
/** Handles a measurement from a weather station. */
def handleReceivedMeasurement(
stationId: Int,
when: Instant,
rainAmount: Double
): Future[Unit] =
for
_ <- database.insertMeasurement:
Measurement(stationId, when, rainAmount)
_ <- database.removeMeasurementsIf: m =>
m.stationId == stationId &&
m.when.compareTo(when) <= 0 &&
Duration.between(m.when, when).compareTo(Duration.ofHours(24)) > 0
yield ()
rainapp/./src/main/scala/weather/WeatherService.scala
💪 Task: Implement predictPrecipitation (24 pts)
Your task is to implement predictPrecipitation, which given a location
and a time, predicts the precipitation (i.e. rain amount) using computeForecast
from RainForecaster:
/** Predicts the rain precipitation given the location and time, based on the
* relevant measurements from weather stations.
*/
def predictPrecipitation(where: Coordinate, when: Instant): Future[Double] =
???
rainapp/./src/main/scala/weather/WeatherService.scala
RainForecasters require a list of relevant measurements and return the predicted precipitation:
trait RainForecaster:
/** Predicts the rain precipitation given the relevant measurements. */
def computeForcast(
where: Coordinate,
when: Instant,
relevantMeasurements: Seq[Measurement]
): Future[Double]
rainapp/./src/main/scala/weather/WeatherService.scala
… where the relevant measurements are the measurements:
- that come from each weather station whose distance is at most 100km away from the prediction location;
- and whose measured time fall within the time range from 12 hours before the prediction time up to the prediction time (both ends are included).
The distance between two coordinates can be calculated by calling
distanceBetween on a Coordinate.
predictPrecipitation should be maximally concurrent for performance
reasons: it should make concurrent queries to the database and the
rain forecaster, when possible.
You can run tests for this task using the testOnly command.
sbt:rainapp> testOnly -- "*predictPrecipitation*"
/** Predicts the rain precipitation given the location and time, based on the
* relevant measurements from weather stations.
*/
def predictPrecipitation(where: Coordinate, when: Instant): Future[Double] =
val twelveHours = Duration.ofHours(12)
for
allStations <- database.getAllStations
qualifiedStations =
allStations.filter(_.location.distanceBetween(where) <= 100)
measurements <- Future.traverse(qualifiedStations): s =>
database.getMeasurementsByStation(s.id)
.map: ms =>
ms.filter: m =>
m.when.compareTo(when) <= 0 &&
Duration.between(m.when, when).compareTo(twelveHours) <= 0
prediction <- forecaster.computeForcast(where, when, measurements.flatten)
yield prediction
rainapp/./src/main/scala/weather/WeatherService.scala
Implementing the trip planner app
The trip planner app can request routes given endpoints from the
PathFinderService, and request predictions from the WeatherService.
Given a precipitation prediction, one can determine whether it is within a
safe amount using the isSafe function:
/** Returns whether we consider the rain precipitation to be safe. */
def isSafe(precipitation: Double): Boolean =
precipitation <= 100
rainapp/./src/main/scala/rainapp/RainApp.scala
(the default one is extremely simple – we can override this function by
extending RainApp)
Our job is to determine whether there exists a point on the route (both ends are included) with an unsafe amount of precipitation.
💪 Task: Implement isSafeTrip (24 pts)
Your task is to implement isSafeTrip, which given the endpoints of a route,
builds a route and check whether there exists a point on the route
with an unsafe amount of precipitation.
/** Checks whether the trip between the given two coordinates at the given
* time is safe from dangerous rain.
*
* A trip is considered safe if the precipitation prediction for all of the
* locations on the path between the endpoints are safe (see [[isSafe]]).
*/
def isSafeTrip(
startWhere: Coordinate,
endWhere: Coordinate,
startWhen: Instant
): Future[Boolean] =
???
rainapp/./src/main/scala/rainapp/RainApp.scala
It should be maximally concurrent, and returns as early as possible:
- This function should make multiple concurrent calls to the services, when possible.
- This function should return as soon as a location with an unsafe amount of rain is found.
You may find the utility function cs214.findFirstCompleted (in cs214/Util.scala) helpful.
You can run tests for this task using the testOnly command.
sbt:rainapp> testOnly -- "*isSafeTrip*"
/** Checks whether the trip between the given two coordinates at the given
* time is safe from dangerous rain.
*
* A trip is considered safe if the precipitation prediction for all of the
* locations on the path between the endpoints are safe (see [[isSafe]]).
*/
def isSafeTrip(
startWhere: Coordinate,
endWhere: Coordinate,
startWhen: Instant
): Future[Boolean] =
for
points <- pathFinder.findPath(startWhere, endWhere)
eachIsSafe = points.map: (p, d) =>
weather.predictPrecipitation(p, startWhen.plus(d))
safe <- cs214.findFirstCompleted(eachIsSafe)(!isSafe(_))
yield !safe.isDefined
rainapp/./src/main/scala/rainapp/RainApp.scala
Putting it all together
Finally, let’s make sure that all of our features work together by running the integration tests.
💪 Task: make sure the integration works! (16 pts)
You can run the integration tests using the testOnly command.
sbt:rainapp> testOnly -- "*integration*"