Skip to content

Commit

Permalink
Merge pull request #3150 from ProjectSidewalk/3079-custom-audit-routes
Browse files Browse the repository at this point in the history
RouteBuilder: a way to create a custom route to follow!
  • Loading branch information
misaugstad authored Feb 25, 2023
2 parents 506fd21 + 20e50ab commit 897a4fa
Show file tree
Hide file tree
Showing 49 changed files with 1,136 additions and 416 deletions.
15 changes: 15 additions & 0 deletions app/controllers/ApplicationController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,21 @@ class ApplicationController @Inject() (implicit val env: Environment[User, Sessi
}
}

/**
* Returns a page that allows a user to build a custom audit route.
*/
def routeBuilder = UserAwareAction.async { implicit request =>
request.identity match {
case Some(user) =>
val timestamp: Timestamp = new Timestamp(Instant.now.toEpochMilli)
val ipAddress: String = request.remoteAddress
WebpageActivityTable.save(WebpageActivity(0, user.userId.toString, ipAddress, "Visit_RouteBuilder", timestamp))
Future.successful(Ok(views.html.routeBuilder(Some(user))))
case None =>
Future.successful(Redirect("/anonSignUp?url=/routeBuilder"))
}
}

/**
* Returns the demo page that contains a cool visualization that is a work-in-progress.
*/
Expand Down
188 changes: 83 additions & 105 deletions app/controllers/AuditController.scala

Large diffs are not rendered by default.

7 changes: 0 additions & 7 deletions app/controllers/RegionController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,6 @@ import models.region.RegionTable.MultiPolygonUtils
class RegionController @Inject() (implicit val env: Environment[User, SessionAuthenticator])
extends Silhouette[User, SessionAuthenticator] with ProvidesHeader {

/**
* This returns the list of difficult neighborhood ids.
*/
def getDifficultNeighborhoods = Action.async { implicit request =>
Future.successful(Ok(Json.obj("regionIds" -> RegionTable.difficultRegionIds)))
}

/**
* Get list of all neighborhoods with a boolean indicating if the given user has fully audited that neighborhood.
*/
Expand Down
50 changes: 50 additions & 0 deletions app/controllers/RouteBuilderController.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package controllers

import javax.inject.Inject
import com.mohiva.play.silhouette.api.{Environment, Silhouette}
import com.mohiva.play.silhouette.impl.authenticators.SessionAuthenticator
import play.api.libs.json._
import controllers.headers.ProvidesHeader
import formats.json.RouteBuilderFormats.NewRoute
import models.daos.slick.DBTableDefinitions.{DBUser, UserTable}
import models.route.{Route, RouteStreet, RouteStreetTable, RouteTable}
import models.user.{User, WebpageActivity, WebpageActivityTable}
import scala.concurrent.Future
import play.api.mvc.BodyParsers
import java.sql.Timestamp
import java.time.Instant

/**
* Holds the HTTP requests associated with managing neighborhoods.
*
* @param env The Silhouette environment.
*/
class RouteBuilderController @Inject() (implicit val env: Environment[User, SessionAuthenticator])
extends Silhouette[User, SessionAuthenticator] with ProvidesHeader {

val anonymousUser: DBUser = UserTable.find("anonymous").get

def saveRoute = UserAwareAction.async(BodyParsers.parse.json) { implicit request =>
val submission = request.body.validate[NewRoute]
submission.fold(
errors => {
Future.successful(BadRequest(Json.obj("status" -> "Error", "message" -> JsError.toFlatJson(errors))))
},
submission => {
val timestamp: Timestamp = new Timestamp(Instant.now.toEpochMilli)
val ipAddress: String = request.remoteAddress
val userIdStr: String = request.identity.map(_.userId.toString).getOrElse(anonymousUser.userId)
WebpageActivityTable.save(WebpageActivity(0, userIdStr, ipAddress, "SaveRoute", timestamp))

// Save new route in the database.
val newRouteId: Int = RouteTable.save(Route(0, userIdStr, submission.regionId, "temp", public = false, deleted = false))
val newRouteStreets: Seq[RouteStreet] = submission.streetIds.zipWithIndex.map { case (streetId, index) =>
RouteStreet(0, newRouteId, streetId, firstStreet = index == 0)
}
RouteStreetTable.saveMultiple(newRouteStreets)

Future.successful(Ok(Json.obj("route_id" -> newRouteId)))
}
)
}
}
6 changes: 3 additions & 3 deletions app/controllers/SignUpController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ class SignUpController @Inject() (
} yield {
// Set the user role, assign the neighborhood to audit, and add to the user_stat table.
UserRoleTable.setRole(user.userId, "Registered", Some(serviceHoursUser))
UserCurrentRegionTable.assignEasyRegion(user.userId)
UserCurrentRegionTable.assignRegion(user.userId)
UserStatTable.addUserStatIfNew(user.userId)

// Log the sign up/in.
Expand Down Expand Up @@ -292,7 +292,7 @@ class SignUpController @Inject() (
} yield {
// Set the user role, assign the neighborhood to audit, and add to the user_stat table.
UserRoleTable.setRole(user.userId, "Turker", Some(false))
UserCurrentRegionTable.assignEasyRegion(user.userId)
UserCurrentRegionTable.assignRegion(user.userId)
UserStatTable.addUserStatIfNew(user.userId)

// Log the sign up/in.
Expand Down Expand Up @@ -323,7 +323,7 @@ class SignUpController @Inject() (
val updatedAuthenticator = authenticator.copy(expirationDate=expirationDate, idleTimeout = Some(2592000))

if (!UserCurrentRegionTable.isAssigned(user.userId)) {
UserCurrentRegionTable.assignEasyRegion(user.userId)
UserCurrentRegionTable.assignRegion(user.userId)
}

// Log the sign in.
Expand Down
43 changes: 42 additions & 1 deletion app/controllers/TaskController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ import models.gsv.{GSVData, GSVDataTable, GSVLink, GSVLinkTable}
import models.label._
import models.mission.{Mission, MissionTable}
import models.region._
import models.route.{AuditTaskUserRouteTable, UserRouteTable}
import models.street.StreetEdgePriorityTable.streetPrioritiesFromIds
import models.street.{StreetEdgePriority, StreetEdgePriorityTable}
import models.street.{StreetEdgeIssue, StreetEdgeIssueTable, StreetEdgePriority, StreetEdgePriorityTable}
import models.user.{User, UserCurrentRegionTable}
import models.utils.CommonUtils.ordered
import play.api.Play.current
Expand Down Expand Up @@ -50,6 +51,35 @@ class TaskController @Inject() (implicit val env: Environment[User, SessionAuthe
}
}

/**
* This method handles a POST request in which user reports a missing Street View image.
*/
def postNoStreetView = UserAwareAction.async(BodyParsers.parse.json) { implicit request =>
var submission = request.body.validate[Int]

submission.fold(
errors => {
Future.successful(BadRequest(Json.obj("status" -> "Error", "message" -> JsError.toFlatJson(errors))))
},
streetEdgeId => {
val userId: String = request.identity match {
case Some(user) => user.userId.toString
case None =>
Logger.warn("User without a user_id reported no SV, but every user should have a user_id.")
val user: Option[DBUser] = UserTable.find("anonymous")
user.get.userId.toString
}
val timestamp: Timestamp = new Timestamp(Instant.now.toEpochMilli)
val ipAddress: String = request.remoteAddress

val issue: StreetEdgeIssue = StreetEdgeIssue(0, streetEdgeId, "GSVNotAvailable", userId, ipAddress, timestamp)
StreetEdgeIssueTable.save(issue)

Future.successful(Ok)
}
)
}

/**
* Get the audit tasks in the given region for the signed in user.
*/
Expand All @@ -63,6 +93,11 @@ class TaskController @Inject() (implicit val env: Environment[User, SessionAuthe
}
}

def getTasksInARoute(userRouteId: Int) = Action.async { implicit request =>
val tasks: List[JsObject] = UserRouteTable.selectTasksInRoute(userRouteId).map(_.toJSON)
Future.successful(Ok(JsArray(tasks)))
}

/**
* Save completion end point of a partially complete task
*/
Expand Down Expand Up @@ -218,6 +253,12 @@ class TaskController @Inject() (implicit val env: Environment[User, SessionAuthe
val auditTaskId: Int = updateAuditTaskTable(userOption, data.auditTask, missionId, data.amtAssignmentId)
updateAuditTaskCompleteness(auditTaskId, data.auditTask, data.incomplete)

// Add to the audit_task_user_route and user_route tables if we are on a route and not in the tutorial.
if (data.userRouteId.isDefined && MissionTable.getMissionType(missionId) == Some("audit")) {
AuditTaskUserRouteTable.insertIfNew(data.userRouteId.get, auditTaskId)
UserRouteTable.updateCompleteness(data.userRouteId.get)
}

// Update MissionStart.
if (data.auditTask.currentMissionStart.isDefined) updateMissionStart(auditTaskId, data.auditTask.currentMissionStart.get)

Expand Down
1 change: 1 addition & 0 deletions app/controllers/UserProfileController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ class UserProfileController @Inject() (implicit val env: Environment[User, Sessi
val properties = Json.obj(
"street_edge_id" -> edge.streetEdgeId,
"way_type" -> edge.wayType,
"region_id" -> edge.regionId,
"audited" -> edge.audited
)
Json.obj("type" -> "Feature", "geometry" -> linestring, "properties" -> properties)
Expand Down
13 changes: 0 additions & 13 deletions app/formats/json/IssueFormats.scala

This file was deleted.

13 changes: 13 additions & 0 deletions app/formats/json/RouteBuilderFormats.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package formats.json

import play.api.libs.json.{JsPath, Reads}
import play.api.libs.functional.syntax._

object RouteBuilderFormats {
case class NewRoute(regionId: Int, streetIds: Seq[Int])

implicit val newRouteReads: Reads[NewRoute] = (
(JsPath \ "region_id").read[Int] and
(JsPath \ "street_ids").read[Seq[Int]]
)(NewRoute.apply _)
}
9 changes: 5 additions & 4 deletions app/formats/json/TaskSubmissionFormats.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package formats.json

import play.api.libs.json.{JsPath, Reads, Json}
import play.api.libs.json.{JsPath, Reads}
import com.vividsolutions.jts.geom._

import scala.collection.immutable.Seq
Expand All @@ -16,7 +16,7 @@ object TaskSubmissionFormats {
case class GSVLinkSubmission(targetGsvPanoramaId: String, yawDeg: Double, description: String)
case class GSVPanoramaSubmission(gsvPanoramaId: String, imageDate: String, imageWidth: Option[Int], imageHeight: Option[Int], tileWidth: Option[Int], tileHeight: Option[Int], links: Seq[GSVLinkSubmission], copyright: String)
case class AuditMissionProgress(missionId: Int, distanceProgress: Option[Float], completed: Boolean, auditTaskId: Option[Int], skipped: Boolean)
case class AuditTaskSubmission(missionProgress: AuditMissionProgress, auditTask: TaskSubmission, labels: Seq[LabelSubmission], interactions: Seq[InteractionSubmission], environment: EnvironmentSubmission, incomplete: Option[IncompleteTaskSubmission], gsvPanoramas: Seq[GSVPanoramaSubmission], amtAssignmentId: Option[Int])
case class AuditTaskSubmission(missionProgress: AuditMissionProgress, auditTask: TaskSubmission, labels: Seq[LabelSubmission], interactions: Seq[InteractionSubmission], environment: EnvironmentSubmission, incomplete: Option[IncompleteTaskSubmission], gsvPanoramas: Seq[GSVPanoramaSubmission], amtAssignmentId: Option[Int], userRouteId: Option[Int])
case class AMTAssignmentCompletionSubmission(assignmentId: Int, completed: Option[Boolean])

implicit val pointReads: Reads[Point] = (
Expand Down Expand Up @@ -126,7 +126,7 @@ object TaskSubmissionFormats {
(JsPath \ "mission_id").read[Int] and
(JsPath \ "distance_progress").readNullable[Float] and
(JsPath \ "completed").read[Boolean] and
(JsPath \ "audit_task_id").read[Option[Int]] and
(JsPath \ "audit_task_id").readNullable[Int] and
(JsPath \ "skipped").read[Boolean]
)(AuditMissionProgress.apply _)

Expand All @@ -138,7 +138,8 @@ object TaskSubmissionFormats {
(JsPath \ "environment").read[EnvironmentSubmission] and
(JsPath \ "incomplete").readNullable[IncompleteTaskSubmission] and
(JsPath \ "gsv_panoramas").read[Seq[GSVPanoramaSubmission]] and
(JsPath \ "amt_assignment_id").readNullable[Int]
(JsPath \ "amt_assignment_id").readNullable[Int] and
(JsPath \ "user_route_id").readNullable[Int]
)(AuditTaskSubmission.apply _)

implicit val amtAssignmentCompletionReads: Reads[AMTAssignmentCompletionSubmission] = (
Expand Down
33 changes: 20 additions & 13 deletions app/models/audit/AuditTaskTable.scala
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ case class NewTask(edgeId: Int, geom: LineString,
completedByAnyUser: Boolean, // Notes if any user has audited this street.
priority: Double,
completed: Boolean, // Notes if the user audited this street before (null if no corresponding user).
auditTaskId: Option[Int], // If it's not actually a "new" task, include the audit_task_id.
currentMissionId: Option[Int],
currentMissionStart: Option[Point] // If a mission was started mid-task, the loc where it started.
) {
Expand All @@ -51,6 +52,7 @@ case class NewTask(edgeId: Int, geom: LineString,
"completed_by_any_user" -> completedByAnyUser,
"priority" -> priority,
"completed" -> completed,
"audit_task_id" -> auditTaskId,
"current_mission_id" -> currentMissionId,
"current_mission_start" -> currentMissionStart.map(p => geojson.LatLng(p.getY, p.getX))
)
Expand Down Expand Up @@ -79,7 +81,7 @@ case class AuditedStreetWithTimestamp(streetEdgeId: Int, auditTaskId: Int,
}
}

case class StreetEdgeWithAuditStatus(streetEdgeId: Int, geom: LineString, wayType: String, audited: Boolean)
case class StreetEdgeWithAuditStatus(streetEdgeId: Int, geom: LineString, regionId: Int, wayType: String, audited: Boolean)

class AuditTaskTable(tag: slick.lifted.Tag) extends Table[AuditTask](tag, Some("sidewalk"), "audit_task") {
def auditTaskId = column[Int]("audit_task_id", O.PrimaryKey, O.AutoInc)
Expand Down Expand Up @@ -121,7 +123,7 @@ object AuditTaskTable {
implicit val newTaskConverter = GetResult[NewTask](r => {
NewTask(r.nextInt, r.nextGeometry[LineString], r.nextFloat, r.nextFloat, r.nextFloat, r.nextFloat, r.nextFloat,
r.nextFloat, r.nextBoolean, r.nextTimestamp, r.nextBoolean, r.nextDouble, r.nextBooleanOption.getOrElse(false),
r.nextIntOption, r.nextGeometryOption[Point])
r.nextIntOption, r.nextIntOption, r.nextGeometryOption[Point])
})

val db = play.api.db.slick.DB
Expand All @@ -131,6 +133,10 @@ object AuditTaskTable {
val streetEdgePriorities = TableQuery[StreetEdgePriorityTable]
val users = TableQuery[UserTable]

val activeTasks = auditTasks
.leftJoin(AuditTaskIncompleteTable.incompletes).on(_.auditTaskId === _.auditTaskId)
.filter(x => !x._1.completed && x._2.auditTaskIncompleteId.?.isEmpty)
.map(_._1)
val completedTasks = auditTasks.filter(_.completed)
val streetEdgesWithoutDeleted = streetEdges.filterNot(_.deleted)
val nonDeletedStreetEdgeRegions = StreetEdgeRegionTable.nonDeletedStreetEdgeRegions
Expand Down Expand Up @@ -319,8 +325,9 @@ object AuditTaskTable {

// Left join list of streets with list of audited streets to record whether each street has been audited.
val streetsWithAuditedStatus = streetEdgesWithoutDeleted
.leftJoin(_distinctCompleted).on(_.streetEdgeId === _)
.map(s => (s._1.streetEdgeId, s._1.geom, s._1.wayType, !s._2.?.isEmpty))
.innerJoin(StreetEdgeRegionTable.streetEdgeRegionTable).on(_.streetEdgeId === _.streetEdgeId)
.leftJoin(_distinctCompleted).on(_._1.streetEdgeId === _)
.map(s => (s._1._1.streetEdgeId, s._1._1.geom, s._1._2.regionId, s._1._1.wayType, !s._2.?.isEmpty))

streetsWithAuditedStatus.list.map(StreetEdgeWithAuditStatus.tupled)
}
Expand Down Expand Up @@ -381,7 +388,7 @@ object AuditTaskTable {
se <- streetEdgesWithoutDeleted if se.streetEdgeId === streetEdgeId
scau <- streetCompletedByAnyUser if se.streetEdgeId === scau._1
sep <- streetEdgePriorities if scau._1 === sep.streetEdgeId
} yield (se.streetEdgeId, se.geom, se.x2, se.y2, se.x1, se.y1, se.x2, se.y2, false, timestamp, scau._2, sep.priority, false, Some(missionId).asColumnOf[Option[Int]], None: Option[Point])
} yield (se.streetEdgeId, se.geom, se.x2, se.y2, se.x1, se.y1, se.x2, se.y2, false, timestamp, scau._2, sep.priority, false, None: Option[Int], Some(missionId).asColumnOf[Option[Int]], None: Option[Point])

NewTask.tupled(edges.first)
}
Expand All @@ -393,7 +400,7 @@ object AuditTaskTable {
val timestamp: Timestamp = new Timestamp(Instant.now.toEpochMilli)
val tutorialTask = streetEdges
.filter(_.streetEdgeId === LabelTable.tutorialStreetId)
.map(e => (e.streetEdgeId, e.geom, e.x2, e.y2, e.x1, e.y1, e.x2, e.y2, false, timestamp, false, 1.0, false, missionId.asColumnOf[Option[Int]], None: Option[Point]))
.map(e => (e.streetEdgeId, e.geom, e.x2, e.y2, e.x1, e.y1, e.x2, e.y2, false, timestamp, false, 1.0, false, None: Option[Int], missionId.asColumnOf[Option[Int]], None: Option[Point]))
NewTask.tupled(tutorialTask.first)
}

Expand All @@ -411,7 +418,7 @@ object AuditTaskTable {
sp <- streetEdgePriorities
se <- edgesInRegion if sp.streetEdgeId === se.streetEdgeId
sc <- streetCompletedByAnyUser if se.streetEdgeId === sc._1
} yield (se.streetEdgeId, se.geom, se.x2, se.y2, se.x1, se.y1, se.x2, se.y2, false, timestamp, sc._2, sp.priority, false, Some(missionId).asColumnOf[Option[Int]], None: Option[Point])
} yield (se.streetEdgeId, se.geom, se.x2, se.y2, se.x1, se.y1, se.x2, se.y2, false, timestamp, sc._2, sp.priority, false, None: Option[Int], Some(missionId).asColumnOf[Option[Int]], None: Option[Point])

// Get the priority of the highest priority task.
val highestPriority: Option[Double] = possibleTasks.map(_._12).max.run
Expand All @@ -430,11 +437,11 @@ object AuditTaskTable {
*/
def selectTaskFromTaskId(taskId: Int): Option[NewTask] = db.withSession { implicit session =>
val newTask = for {
at <- auditTasks if at.auditTaskId === taskId
at <- activeTasks if at.auditTaskId === taskId
se <- streetEdges if at.streetEdgeId === se.streetEdgeId
sp <- streetEdgePriorities if se.streetEdgeId === sp.streetEdgeId
sc <- streetCompletedByAnyUser if sp.streetEdgeId === sc._1
} yield (se.streetEdgeId, se.geom, at.currentLng, at.currentLat, se.x1, se.y1, se.x2, se.y2, at.startPointReversed, at.taskStart, sc._2, sp.priority, at.completed, at.currentMissionId, at.currentMissionStart)
} yield (se.streetEdgeId, se.geom, at.currentLng, at.currentLat, se.x1, se.y1, se.x2, se.y2, at.startPointReversed, at.taskStart, sc._2, sp.priority, at.completed, at.auditTaskId.?, at.currentMissionId, at.currentMissionStart)

newTask.firstOption.map(NewTask.tupled)
}
Expand All @@ -447,20 +454,20 @@ object AuditTaskTable {

val edgesInRegion = nonDeletedStreetEdgeRegions.filter(_.regionId === regionId)

// Get street_edge_id, task_start, current_mission_id, and current_mission_start for streets the user has audited.
// If there are multiple audit_tasks for the same street, choose most recent one (one w/ the highest audit_task_id).
// Get street_edge_id, task_start, audit_task_id, current_mission_id, and current_mission_start for streets the user
// has audited. If there are multiple for the same street, choose most recent (one w/ the highest audit_task_id).
val userCompletedStreets = completedTasks
.filter(_.userId === user.toString)
.groupBy(_.streetEdgeId).map(_._2.map(_.auditTaskId).max)
.innerJoin(auditTasks).on(_ === _.auditTaskId)
.map(t => (t._2.streetEdgeId, t._2.taskStart, t._2.currentMissionId, t._2.currentMissionStart))
.map(t => (t._2.streetEdgeId, t._2.taskStart, t._2.auditTaskId, t._2.currentMissionId, t._2.currentMissionStart))

val tasks = for {
(ser, ucs) <- edgesInRegion.leftJoin(userCompletedStreets).on(_.streetEdgeId === _._1)
se <- streetEdges if ser.streetEdgeId === se.streetEdgeId
sep <- streetEdgePriorities if se.streetEdgeId === sep.streetEdgeId
scau <- streetCompletedByAnyUser if sep.streetEdgeId === scau._1
} yield (se.streetEdgeId, se.geom, se.x2, se.y2, se.x1, se.y1, se.x2, se.y2, false, ucs._2.?.getOrElse(timestamp), scau._2, sep.priority, ucs._1.?.isDefined, ucs._3, ucs._4)
} yield (se.streetEdgeId, se.geom, se.x2, se.y2, se.x1, se.y1, se.x2, se.y2, false, ucs._2.?.getOrElse(timestamp), scau._2, sep.priority, ucs._1.?.isDefined, ucs._3.?, ucs._4, ucs._5)

tasks.list.map(NewTask.tupled(_))
}
Expand Down
Loading

0 comments on commit 897a4fa

Please sign in to comment.