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.scala
src/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:

Architecture of Trip Planner App

In particular,

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:

  1. 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.

  2. Perform rain prediction by calling the computeForecast method of the RainForecaster with the desired location, time, and a set of relevant measurements queried from the Database.

    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:

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:

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:

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*"