diff --git a/Gruntfile.js b/Gruntfile.js index 2e3a68f0bc..599f4beb77 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -32,7 +32,8 @@ module.exports = function(grunt) { 'public/javascripts/common/UtilitiesMath.js', 'public/javascripts/common/UtilitiesPanomarker.js', 'public/javascripts/common/UtilitiesShape.js', - 'public/javascripts/common/UtilitiesSidewalk.js' + 'public/javascripts/common/UtilitiesSidewalk.js', + 'public/javascripts/common/GSVInfoPopover.js' ], dest: 'public/javascripts/SVLabel/build/SVLabel.js' }, @@ -70,7 +71,8 @@ module.exports = function(grunt) { 'public/javascripts/SVValidate/src/util/*.js', 'public/javascripts/SVValidate/src/zoom/*.js', 'public/javascripts/common/Panomarker.js', - 'public/javascripts/common/UtilitiesSidewalk.js' + 'public/javascripts/common/UtilitiesSidewalk.js', + 'public/javascripts/common/GSVInfoPopover.js' ], dest: 'public/javascripts/SVValidate/build/SVValidate.js' }, @@ -83,7 +85,8 @@ module.exports = function(grunt) { 'public/javascripts/Gallery/src/displays/*.js', 'public/javascripts/Gallery/src/modal/*.js', 'public/javascripts/Gallery/src/*.js', - 'public/javascripts/common/Panomarker.js' + 'public/javascripts/common/Panomarker.js', + 'public/javascripts/common/GSVInfoPopover.js' ], dest: 'public/javascripts/Gallery/build/Gallery.js' } diff --git a/app/formats/json/LabelFormat.scala b/app/formats/json/LabelFormat.scala index 1018ed311b..5837ad41cf 100644 --- a/app/formats/json/LabelFormat.scala +++ b/app/formats/json/LabelFormat.scala @@ -71,6 +71,8 @@ object LabelFormat { "severity" -> labelMetadata.severity, "temporary" -> labelMetadata.temporary, "description" -> labelMetadata.description, + "street_edge_id" -> labelMetadata.streetEdgeId, + "region_id" -> labelMetadata.regionId, "user_validation" -> labelMetadata.userValidation.map(LabelValidationTable.validationOptions.get), "tags" -> labelMetadata.tags ) @@ -82,14 +84,16 @@ object LabelFormat { "gsv_panorama_id" -> labelMetadata.gsvPanoramaId, "tutorial" -> labelMetadata.tutorial, "image_date" -> labelMetadata.imageDate, - "heading" -> labelMetadata.heading, - "pitch" -> labelMetadata.pitch, - "zoom" -> labelMetadata.zoom, + "heading" -> labelMetadata.headingPitchZoom._1, + "pitch" -> labelMetadata.headingPitchZoom._2, + "zoom" -> labelMetadata.headingPitchZoom._3, "canvas_x" -> labelMetadata.canvasXY._1, "canvas_y" -> labelMetadata.canvasXY._2, - "canvas_width" -> labelMetadata.canvasWidth, - "canvas_height" -> labelMetadata.canvasHeight, + "canvas_width" -> labelMetadata.canvasWidthHeight._1, + "canvas_height" -> labelMetadata.canvasWidthHeight._2, "audit_task_id" -> labelMetadata.auditTaskId, + "street_edge_id" -> labelMetadata.streetEdgeId, + "region_id" -> labelMetadata.regionId, "user_id" -> labelMetadata.userId, "username" -> labelMetadata.username, "timestamp" -> labelMetadata.timestamp, @@ -113,13 +117,15 @@ object LabelFormat { "gsv_panorama_id" -> labelMetadata.gsvPanoramaId, "tutorial" -> labelMetadata.tutorial, "image_date" -> labelMetadata.imageDate, - "heading" -> labelMetadata.heading, - "pitch" -> labelMetadata.pitch, - "zoom" -> labelMetadata.zoom, + "heading" -> labelMetadata.headingPitchZoom._1, + "pitch" -> labelMetadata.headingPitchZoom._2, + "zoom" -> labelMetadata.headingPitchZoom._3, "canvas_x" -> labelMetadata.canvasXY._1, "canvas_y" -> labelMetadata.canvasXY._2, - "canvas_width" -> labelMetadata.canvasWidth, - "canvas_height" -> labelMetadata.canvasHeight, + "canvas_width" -> labelMetadata.canvasWidthHeight._1, + "canvas_height" -> labelMetadata.canvasWidthHeight._2, + "street_edge_id" -> labelMetadata.streetEdgeId, + "region_id" -> labelMetadata.regionId, "timestamp" -> labelMetadata.timestamp, "label_type_key" -> labelMetadata.labelTypeKey, "label_type_value" -> labelMetadata.labelTypeValue, diff --git a/app/models/audit/AuditTaskTable.scala b/app/models/audit/AuditTaskTable.scala index 598e64b8fd..9ad99a4b0e 100644 --- a/app/models/audit/AuditTaskTable.scala +++ b/app/models/audit/AuditTaskTable.scala @@ -10,6 +10,7 @@ import models.utils.MyPostgresDriver.simple._ import models.daos.slick.DBTableDefinitions.{DBUser, UserTable} import models.label.{LabelTable, LabelTypeTable} import models.street.StreetEdgePriorityTable +import models.region.RegionTable import models.user.{UserRoleTable, UserStatTable} import play.api.libs.json._ import play.api.Play.current @@ -18,7 +19,7 @@ import scala.slick.lifted.ForeignKeyQuery import scala.slick.jdbc.{GetResult, StaticQuery => Q} case class AuditTask(auditTaskId: Int, amtAssignmentId: Option[Int], userId: String, streetEdgeId: Int, taskStart: Timestamp, taskEnd: Option[Timestamp], completed: Boolean, currentLat: Float, currentLng: Float, startPointReversed: Boolean) -case class NewTask(edgeId: Int, geom: LineString, +case class NewTask(edgeId: Int, geom: LineString, regionId: Int, currentLng: Float, currentLat: Float, x1: Float, y1: Float, x2: Float, y2: Float, startPointReversed: Boolean, // Did we start at x1,y1 instead of x2,y2? taskStart: Timestamp, @@ -35,6 +36,7 @@ case class NewTask(edgeId: Int, geom: LineString, val linestring: geojson.LineString[geojson.LatLng] = geojson.LineString(latlngs) val properties = Json.obj( "street_edge_id" -> edgeId, + "region_id" -> regionId, "current_lng" -> currentLng, "current_lat" -> currentLat, "x1" -> x1, @@ -108,6 +110,7 @@ object AuditTaskTable { implicit val newTaskConverter = GetResult[NewTask](r => { val edgeId = r.nextInt val geom = r.nextGeometry[LineString] + val regionId = r.nextInt val currentLng = r.nextFloat val currentLat = r.nextFloat val x1 = r.nextFloat @@ -119,7 +122,7 @@ object AuditTaskTable { val completedByAnyUser = r.nextBoolean val priority = r.nextDouble val completed = r.nextBooleanOption.getOrElse(false) - NewTask(edgeId, geom, currentLng, currentLat, x1, y1, x2, y2, startPointReversed, taskStart, completedByAnyUser, priority, completed) + NewTask(edgeId, geom, regionId, currentLng, currentLat, x1, y1, x2, y2, startPointReversed, taskStart, completedByAnyUser, priority, completed) }) val db = play.api.db.slick.DB @@ -128,6 +131,7 @@ object AuditTaskTable { val streetEdges = TableQuery[StreetEdgeTable] val streetEdgePriorities = TableQuery[StreetEdgePriorityTable] val users = TableQuery[UserTable] + val regions = TableQuery[StreetEdgeRegionTable] val completedTasks = auditTasks.filter(_.completed) val streetEdgesWithoutDeleted = streetEdges.filterNot(_.deleted) @@ -370,9 +374,10 @@ object AuditTaskTable { // Join with other queries to get completion count and priority for each of the street edges. val edges = for { se <- streetEdgesWithoutDeleted if se.streetEdgeId === streetEdgeId + re <- regions if se.streetEdgeId === re.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, userCompleted) + } yield (se.streetEdgeId, se.geom, re.regionId, se.x2, se.y2, se.x1, se.y1, se.x2, se.y2, false, timestamp, scau._2, sep.priority, userCompleted) NewTask.tupled(edges.first) } @@ -390,15 +395,16 @@ object AuditTaskTable { val possibleTasks = for { sp <- streetEdgePriorities se <- edgesInRegion if sp.streetEdgeId === se.streetEdgeId + re <- regions if se.streetEdgeId === re.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) + } yield (se.streetEdgeId, se.geom, re.regionId, se.x2, se.y2, se.x1, se.y1, se.x2, se.y2, false, timestamp, sc._2, sp.priority, false) // Get the priority of the highest priority task. - val highestPriority: Option[Double] = possibleTasks.map(_._12).max.run + val highestPriority: Option[Double] = possibleTasks.map(_._13).max.run // Get list of tasks that have this priority. val highestPriorityTasks: Option[List[NewTask]] = highestPriority.map { highPriority => - possibleTasks.filter(_._12 === highPriority).list.map(NewTask.tupled) + possibleTasks.filter(_._13 === highPriority).list.map(NewTask.tupled) } // Choose one of the highest priority tasks at random. @@ -414,9 +420,10 @@ object AuditTaskTable { val newTask = for { at <- auditTasks if at.auditTaskId === taskId se <- streetEdges if at.streetEdgeId === se.streetEdgeId + re <- regions if se.streetEdgeId === re.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, timestamp, sc._2, sp.priority, false) + } yield (se.streetEdgeId, se.geom, re.regionId, at.currentLng, at.currentLat, se.x1, se.y1, se.x2, se.y2, at.startPointReversed, timestamp, sc._2, sp.priority, false) newTask.list.map(NewTask.tupled).headOption } @@ -430,9 +437,10 @@ object AuditTaskTable { val tasks = for { ser <- nonDeletedStreetEdgeRegions if ser.regionId === regionId se <- streetEdges if ser.streetEdgeId === se.streetEdgeId + re <- regions if se.streetEdgeId === re.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, timestamp, scau._2, sep.priority, false) + } yield (se.streetEdgeId, se.geom, re.regionId, se.x2, se.y2, se.x1, se.y1, se.x2, se.y2, false, timestamp, scau._2, sep.priority, false) tasks.list.map(NewTask.tupled(_)) } @@ -450,10 +458,11 @@ object AuditTaskTable { val tasks = for { (ser, ucs) <- edgesInRegion.leftJoin(userCompletedStreets).on(_.streetEdgeId === _._1) se <- streetEdges if ser.streetEdgeId === se.streetEdgeId + re <- regions if se.streetEdgeId === re.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, timestamp, scau._2, sep.priority, ucs._2.?.getOrElse(false)) + se.streetEdgeId, se.geom, re.regionId, se.x2, se.y2, se.x1, se.y1, se.x2, se.y2, false, timestamp, scau._2, sep.priority, ucs._2.?.getOrElse(false)) tasks.list.map(NewTask.tupled(_)) } diff --git a/app/models/label/LabelTable.scala b/app/models/label/LabelTable.scala index 8aac535cc4..f0dc6397c2 100644 --- a/app/models/label/LabelTable.scala +++ b/app/models/label/LabelTable.scala @@ -142,12 +142,12 @@ object LabelTable { case class LabelCountPerDay(date: String, count: Int) - case class LabelMetadata(labelId: Int, gsvPanoramaId: String, tutorial: Boolean, imageDate: String, heading: Float, - pitch: Float, zoom: Int, canvasXY: (Int, Int), canvasWidth: Int, canvasHeight: Int, - auditTaskId: Int, userId: String, username: String, timestamp: java.sql.Timestamp, - labelTypeKey: String, labelTypeValue: String, severity: Option[Int], temporary: Boolean, - description: Option[String], userValidation: Option[Int], validations: Map[String, Int], - tags: List[String]) + case class LabelMetadata(labelId: Int, gsvPanoramaId: String, tutorial: Boolean, imageDate: String, + headingPitchZoom: (Float, Float, Int), canvasXY: (Int, Int), canvasWidthHeight: (Int, Int), + auditTaskId: Int, streetEdgeId: Int, regionId: Int, userId: String, username: String, + timestamp: java.sql.Timestamp, labelTypeKey: String, labelTypeValue: String, + severity: Option[Int], temporary: Boolean, description: Option[String], + userValidation: Option[Int], validations: Map[String, Int], tags: List[String]) case class LabelMetadataUserDash(labelId: Int, gsvPanoramaId: String, heading: Float, pitch: Float, zoom: Int, canvasX: Int, canvasY: Int, canvasWidth: Int, canvasHeight: Int, labelType: String, @@ -158,13 +158,14 @@ object LabelTable { timestamp: java.sql.Timestamp, heading: Float, pitch: Float, zoom: Int, canvasX: Int, canvasY: Int, canvasWidth: Int, canvasHeight: Int, severity: Option[Int], temporary: Boolean, description: Option[String], - userValidation: Option[Int], tags: List[String]) extends BasicLabelMetadata + streetEdgeId: Int, regionId: Int, userValidation: Option[Int], tags: List[String]) extends BasicLabelMetadata case class LabelValidationMetadataWithoutTags(labelId: Int, labelType: String, gsvPanoramaId: String, imageDate: String, timestamp: java.sql.Timestamp, heading: Float, pitch: Float, zoom: Int, canvasX: Int, canvasY: Int, canvasWidth: Int, canvasHeight: Int, severity: Option[Int], temporary: Boolean, - description: Option[String], userValidation: Option[Int]) extends BasicLabelMetadata + description: Option[String], streetEdgeId: Int, regionId: Int, + userValidation: Option[Int]) extends BasicLabelMetadata case class ResumeLabelMetadata(labelData: Label, labelType: String, pointData: LabelPoint, svImageWidth: Int, svImageHeight: Int, tagIds: List[Int]) @@ -176,9 +177,9 @@ object LabelTable { implicit val labelMetadataWithValidationConverter = GetResult[LabelMetadata](r => LabelMetadata( - r.nextInt, r.nextString, r.nextBoolean, r.nextString, r.nextFloat, r.nextFloat, r.nextInt, (r.nextInt, r.nextInt), - r.nextInt, r.nextInt, r.nextInt, r.nextString, r.nextString, r.nextTimestamp, r.nextString, r.nextString, - r.nextIntOption, r.nextBoolean, r.nextStringOption, r.nextIntOption, + r.nextInt, r.nextString, r.nextBoolean, r.nextString, (r.nextFloat, r.nextFloat, r.nextInt), + (r.nextInt, r.nextInt), (r.nextInt, r.nextInt), r.nextInt, r.nextInt, r.nextInt, r.nextString, r.nextString, + r.nextTimestamp, r.nextString, r.nextString, r.nextIntOption, r.nextBoolean, r.nextStringOption, r.nextIntOption, r.nextString.split(',').map(x => x.split(':')).map { y => (y(0), y(1).toInt) }.toMap, r.nextStringOption.map(tags => tags.split(",").toList).getOrElse(List()) ) @@ -188,15 +189,15 @@ object LabelTable { LabelValidationMetadataWithoutTags( r.nextInt, r.nextString, r.nextString, r.nextString, r.nextTimestamp, r.nextFloat, r.nextFloat, r.nextInt, r.nextInt, r.nextInt, r.nextInt, r.nextInt, r.nextIntOption, r.nextBoolean, - r.nextStringOption, r.nextIntOption + r.nextStringOption, r.nextInt, r.nextInt, r.nextIntOption ) ) implicit val labelValidationMetadataConverter = GetResult[LabelValidationMetadata](r => LabelValidationMetadata( r.nextInt, r.nextString, r.nextString, r.nextString, r.nextTimestamp, r.nextFloat, r.nextFloat, r.nextInt, - r.nextInt, r.nextInt, r.nextInt, r.nextInt, r.nextIntOption, r.nextBoolean, r.nextStringOption, r.nextIntOption, - r.nextStringOption.map(tags => tags.split(",").toList).getOrElse(List()) + r.nextInt, r.nextInt, r.nextInt, r.nextInt, r.nextIntOption, r.nextBoolean, r.nextStringOption, r.nextInt, + r.nextInt, r.nextIntOption, r.nextStringOption.map(tags => tags.split(",").toList).getOrElse(List()) ) ) @@ -395,6 +396,8 @@ object LabelTable { | lp.canvas_width, | lp.canvas_height, | lb1.audit_task_id, + | lb1.street_edge_id, + | ser.region_id, | u.user_id, | u.username, | lb1.time_created, @@ -409,6 +412,7 @@ object LabelTable { |FROM label AS lb1, | gsv_data, | audit_task AS at, + | street_edge_region AS ser, | sidewalk_user AS u, | label_point AS lp, | ( @@ -443,6 +447,7 @@ object LabelTable { | AND lb1.audit_task_id = at.audit_task_id | AND lb1.label_id = lb_big.label_id | AND at.user_id = u.user_id + | AND lb1.street_edge_id = ser.street_edge_id | AND lb1.label_id = lp.label_id | AND lb1.label_id = val.label_id | $labelFilter @@ -512,7 +517,8 @@ object LabelTable { s"""SELECT label.label_id, label_type.label_type, label.gsv_panorama_id, gsv_data.image_date, | label.time_created, label_point.heading, label_point.pitch, label_point.zoom, label_point.canvas_x, | label_point.canvas_y, label_point.canvas_width, label_point.canvas_height, label.severity, - | label.temporary, label.description, user_validation.validation_result, the_tags.tag_list + | label.temporary, label.description, label.street_edge_id, street_edge_region.region_id, + | user_validation.validation_result, the_tags.tag_list |FROM label |INNER JOIN label_type ON label.label_type_id = label_type.label_type_id |INNER JOIN label_point ON label.label_id = label_point.label_id @@ -520,6 +526,7 @@ object LabelTable { |INNER JOIN mission ON label.mission_id = mission.mission_id |INNER JOIN user_stat ON mission.user_id = user_stat.user_id |INNER JOIN audit_task ON label.audit_task_id = audit_task.audit_task_id + |INNER JOIN street_edge_region ON label.street_edge_id = street_edge_region.street_edge_id |LEFT JOIN ( | -- This subquery counts how many of each users' labels have been validated. If it's less than 50, then we | -- need more validations from them in order to infer worker quality, and they therefore get priority. @@ -615,12 +622,13 @@ object LabelTable { _tags <- tagTable if _labelTags.tagId === _tags.tagId && ((_tags.tag inSet tags) || tags.isEmpty) _a <- auditTasks if _lb.auditTaskId === _a.auditTaskId _us <- UserStatTable.userStats if _a.userId === _us.userId + _ser <- StreetEdgeRegionTable.streetEdgeRegionTable if _lb.streetEdgeId === _ser.streetEdgeId if _lb.labelTypeId === labelTypeId if _gd.expired === false if _us.highQuality || (_lb.correct.isDefined && _lb.correct === true) if _lb.disagreeCount < 3 || _lb.disagreeCount < _lb.agreeCount * 2 if _lb.severity.isEmpty || (_lb.severity inSet severity) - } yield (_lb, _lp, _lt, _gd) + } yield (_lb, _lp, _lt, _gd, _ser) // Join with the validations that the user has given. val userValidations = validationsFromUser(userId) @@ -628,7 +636,7 @@ object LabelTable { (l, v) <- _galleryLabels.leftJoin(userValidations).on(_._1.labelId === _._1) } yield (l._1.labelId, l._3.labelType, l._1.gsvPanoramaId, l._4.imageDate, l._1.timeCreated, l._2.heading, l._2.pitch, l._2.zoom, l._2.canvasX, l._2.canvasY, l._2.canvasWidth, l._2.canvasHeight, l._1.severity, - l._1.temporary, l._1.description, v._2.?) + l._1.temporary, l._1.description, l._1.streetEdgeId, l._5.regionId, v._2.?) // Remove duplicates that we got from joining with the `label_tag` table. val uniqueLabels = addValidations.groupBy(x => x).map(_._1) @@ -658,10 +666,11 @@ object LabelTable { _gd <- gsvData if _lb.gsvPanoramaId === _gd.gsvPanoramaId _a <- auditTasks if _lb.auditTaskId === _a.auditTaskId _us <- UserStatTable.userStats if _a.userId === _us.userId + _ser <- StreetEdgeRegionTable.streetEdgeRegionTable if _lb.streetEdgeId === _ser.streetEdgeId if _gd.expired === false if _us.highQuality || (_lb.correct.isDefined && _lb.correct === true) if _lb.disagreeCount < 3 || _lb.disagreeCount < _lb.agreeCount * 2 - } yield (_lb, _lp, _lt, _gd) + } yield (_lb, _lp, _lt, _gd, _ser) // If severities are specified, filter by whether a label has a valid severity. val _labels = if (severity.isDefined && severity.get.nonEmpty) @@ -675,7 +684,7 @@ object LabelTable { (l, v) <- _labels.leftJoin(userValidations).on(_._1.labelId === _._1) } yield (l._1.labelId, l._3.labelType, l._1.gsvPanoramaId, l._4.imageDate, l._1.timeCreated, l._2.heading, l._2.pitch, l._2.zoom, l._2.canvasX, l._2.canvasY, l._2.canvasWidth, l._2.canvasHeight, l._1.severity, - l._1.temporary, l._1.description, v._2.?) + l._1.temporary, l._1.description, l._1.streetEdgeId, l._5.regionId, v._2.?) // Run query, group by label type, and randomize order. val potentialLabels: Map[String, List[LabelValidationMetadataWithoutTags]] = @@ -708,11 +717,12 @@ object LabelTable { _gd <- gsvData if _lb.gsvPanoramaId === _gd.gsvPanoramaId _a <- auditTasks if _lb.auditTaskId === _a.auditTaskId _us <- UserStatTable.userStats if _a.userId === _us.userId + _ser <- StreetEdgeRegionTable.streetEdgeRegionTable if _lb.streetEdgeId === _ser.streetEdgeId if _lb.labelTypeId === labelTypeId if _gd.expired === false if _us.highQuality || (_lb.correct.isDefined && _lb.correct === true) if _lb.disagreeCount < 3 || _lb.disagreeCount < _lb.agreeCount * 2 - } yield (_lb, _lp, _lt, _gd) + } yield (_lb, _lp, _lt, _gd, _ser) // Join with the validations that the user has given. val userValidations = validationsFromUser(userId) @@ -720,7 +730,7 @@ object LabelTable { (l, v) <- _labels.leftJoin(userValidations).on(_._1.labelId === _._1) } yield (l._1.labelId, l._3.labelType, l._1.gsvPanoramaId, l._4.imageDate, l._1.timeCreated, l._2.heading, l._2.pitch, l._2.zoom, l._2.canvasX, l._2.canvasY, l._2.canvasWidth, l._2.canvasHeight, l._1.severity, - l._1.temporary, l._1.description, v._2.?) + l._1.temporary, l._1.description, l._1.streetEdgeId, l._5.regionId, v._2.?) // Randomize and convert to LabelValidationMetadataWithoutTags. val newRandomLabelsList = addValidations.sortBy(x => rand).list.map(LabelValidationMetadataWithoutTags.tupled) @@ -889,7 +899,7 @@ object LabelTable { LabelValidationMetadata( label.labelId, label.labelType, label.gsvPanoramaId, label.imageDate, label.timestamp, label.heading, label.pitch, label.zoom, label.canvasX, label.canvasY, label.canvasWidth, label.canvasHeight, label.severity, - label.temporary, label.description, label.userValidation, tags + label.temporary, label.description, label.streetEdgeId, label.regionId, label.userValidation, tags ) } diff --git a/app/views/audit.scala.html b/app/views/audit.scala.html index 9708953690..b3fcee86ed 100644 --- a/app/views/audit.scala.html +++ b/app/views/audit.scala.html @@ -902,9 +902,15 @@

@Messages("surface.problem")

} $(document).ready(function() { - // Solutions to annoying text selection cursor. - // http://forum.jquery.com/topic/chrome-text-select-cursor-on-drag - document.onselectstart = function () { return false; }; + // Prevents text selection with cursor. Fixes https://github.com/ProjectSidewalk/SidewalkWebpage/issues/121. + // We also add the 'audit-selectable' class to a parent of any element that we want to have selectable. + document.onselectstart = function (e) { + if ($(e.target).closest('.audit-selectable').length > 0) { + return true; + } else { + return false; + } + }; enableTouchSupport(); // Advanced Neighborhood Overlay diff --git a/app/views/footer.scala.html b/app/views/footer.scala.html index 55808b631b..d409d3a53d 100644 --- a/app/views/footer.scala.html +++ b/app/views/footer.scala.html @@ -3,7 +3,7 @@ @()(implicit lang: Lang) -' + - '' - } else { - modalText += '' + - '' + - '' + - '' + - '' - } + '' + self.modal = $(modalText); self.panorama = AdminPanorama(self.modal.find("#svholder")[0], self.modal.find("#validation-input-holder"), admin); @@ -152,8 +168,13 @@ function AdminGSVLabelView(admin) { self.modalValidations = self.modal.find("#label-validations"); self.modalImageDate = self.modal.find("#image-date"); self.modalTask = self.modal.find("#task"); - self.modalLabelId = self.modal.find("#label-id"); self.modalPanoId = self.modal.find('#pano-id'); + self.modalGsvLink = self.modal.find('#view-in-gsv'); + self.modalLat = self.modal.find('#lat'); + self.modalLng = self.modal.find('#lng'); + self.modalLabelId = self.modal.find("#label-id"); + self.modalStreetId = self.modal.find('#street-id'); + self.modalRegionId = self.modal.find('#region-id'); } /** @@ -305,8 +326,16 @@ function AdminGSVLabelView(admin) { } function _handleData(labelMetadata) { + // Pass a callback function that fills in the pano lat/lng. + var panoCallback = function () { + var lat = self.panorama.panorama.getPosition().lat(); + var lng = self.panorama.panorama.getPosition().lng(); + self.modalGsvLink.html(`${i18next.t('common:gsv-info.view-in-gsv')}`); + self.modalLat.html(lat.toFixed(8) + '°'); + self.modalLng.html(lng.toFixed(8) + '°'); + } self.panorama.setPano(labelMetadata['gsv_panorama_id'], labelMetadata['heading'], - labelMetadata['pitch'], labelMetadata['zoom']); + labelMetadata['pitch'], labelMetadata['zoom'], panoCallback); var adminPanoramaLabel = AdminPanoramaLabel(labelMetadata['label_id'], labelMetadata['label_type_key'], labelMetadata['canvas_x'], labelMetadata['canvas_y'], @@ -321,17 +350,19 @@ function AdminGSVLabelView(admin) { var labelDate = moment(new Date(labelMetadata['timestamp'])); var imageDate = moment(new Date(labelMetadata['image_date'])); self.modalTitle.html('Label Type: ' + labelMetadata['label_type_value']); - self.modalTimestamp.html(labelDate.format('LL, LT') + " (" + labelDate.fromNow() + ")"); self.modalLabelTypeValue.html(labelMetadata['label_type_value']); self.modalSeverity.html(labelMetadata['severity'] != null ? labelMetadata['severity'] : "No severity"); self.modalTemporary.html(labelMetadata['temporary'] ? i18next.t('common:yes'): i18next.t('common:no')); self.modalTags.html(labelMetadata['tags'].join(', ')); // Join to format using commas and spaces. self.modalDescription.html(labelMetadata['description'] != null ? labelMetadata['description'] : i18next.t('common:no-description')); self.modalValidations.html(validationsText); + self.modalTimestamp.html(labelDate.format('LL, LT') + " (" + labelDate.fromNow() + ")"); self.modalImageDate.html(imageDate.format('MMMM YYYY')); self.modalPanoId.html(labelMetadata['gsv_panorama_id']); + self.modalLabelId.html(labelMetadata['label_id']); + self.modalStreetId.html(labelMetadata['street_edge_id']); + self.modalRegionId.html(labelMetadata['region_id']); if (self.admin) { - self.modalLabelId.html(labelMetadata['label_id']); self.modalTask.html(""+ labelMetadata['audit_task_id']+" by " + labelMetadata['username'] + ""); diff --git a/public/javascripts/Admin/src/Admin.Panorama.js b/public/javascripts/Admin/src/Admin.Panorama.js index 023ff034c6..709e3b0583 100644 --- a/public/javascripts/Admin/src/Admin.Panorama.js +++ b/public/javascripts/Admin/src/Admin.Panorama.js @@ -115,8 +115,9 @@ function AdminPanorama(svHolder, buttonHolder, admin) { * @param heading * @param pitch * @param zoom + * @param callbackParam */ - function setPano(panoId, heading, pitch, zoom) { + function setPano(panoId, heading, pitch, zoom, callbackParam) { if (typeof google != "undefined") { self.panorama.registerPanoProvider(function(pano) { if (pano === 'tutorial' || pano === 'afterWalkTutorial') { @@ -169,6 +170,7 @@ function AdminPanorama(svHolder, buttonHolder, admin) { } else { setTimeout(callback, 200, n - 1); } + callbackParam(); } setTimeout(callback, 200, 10); } @@ -326,7 +328,43 @@ function AdminPanorama(svHolder, buttonHolder, admin) { } } - //init + /** + * Returns the panorama ID for the current panorama. + * @returns {google.maps.StreetViewPanorama} Google StreetView Panorama Id + */ + function getPanoId() { + return self.panorama.getPano(); + } + + /** + * Returns the lat lng of this panorama. Note that sometimes position is null/undefined + * (probably a bug in GSV), so sometimes this function returns null. + * @returns {{lat, lng}} + */ + function getPos() { + let position = self.panorama.getPosition(); + return (position) ? {'lat': position.lat(), 'lng': position.lng()} : null; + } + + /** + * Returns the pov of the viewer. + * @returns {{heading: float, pitch: float, zoom: float}} + */ + function getPov() { + let pov = self.panorama.getPov(); + + // Pov can be less than 0. So adjust it. + while (pov.heading < 0) { + pov.heading += 360; + } + + // Pov can be more than 360. Adjust it. + while (pov.heading > 360) { + pov.heading -= 360; + } + return pov; + } + _init(); self.setPov = setPov; @@ -334,5 +372,9 @@ function AdminPanorama(svHolder, buttonHolder, admin) { self.setLabel = setLabel; self.renderLabel = renderLabel; self.getOriginalPosition = getOriginalPosition; + self.getPanoId = getPanoId; + self.getPosition = getPos; + self.getPov = getPov; + return self; } diff --git a/public/javascripts/Gallery/css/modal.css b/public/javascripts/Gallery/css/modal.css index 9a78245ad5..6c90f2baef 100644 --- a/public/javascripts/Gallery/css/modal.css +++ b/public/javascripts/Gallery/css/modal.css @@ -25,6 +25,17 @@ color: #2d2a3f; } +.label-timestamp { + display: flex; +} + +#gsv-info-button { + width: 0.9vw; + height: 0.9vw; + margin-left: 5px; + margin-top: 2px; +} + .modal-severity-header, .modal-tag-header, .modal-temporary-header, .modal-description-header { font-weight: bold; color: #2d2a3f; @@ -96,7 +107,8 @@ justify-content: space-between; width: 100%; font-size: .8vw; - padding-bottom: 1%; + padding-bottom: 0.9%; + padding-top: 0.1%; } .gallery-modal-info-severity { diff --git a/public/javascripts/Gallery/src/Main.js b/public/javascripts/Gallery/src/Main.js index bbbb94d538..084e0b2d02 100644 --- a/public/javascripts/Gallery/src/Main.js +++ b/public/javascripts/Gallery/src/Main.js @@ -73,6 +73,7 @@ function Main (params) { // sg.cardSortMenu = new CardSortMenu(sg.ui.cardSortMenu); sg.tagContainer = new CardFilter(sg.ui.cardFilter, sg.labelTypeMenu, sg.cityMenu); sg.cardContainer = new CardContainer(sg.ui.cardContainer); + sg.modal = sg.cardContainer.getModal; // Initialize data collection. sg.form = new Form(params.dataStoreUrl, params.beaconDataStoreUrl) sg.tracker = new Tracker(); diff --git a/public/javascripts/Gallery/src/cards/Card.js b/public/javascripts/Gallery/src/cards/Card.js index e3eb5ae9cb..3fcea247bb 100644 --- a/public/javascripts/Gallery/src/cards/Card.js +++ b/public/javascripts/Gallery/src/cards/Card.js @@ -33,6 +33,8 @@ function Card (params, imageUrl, modal) { severity: undefined, temporary: undefined, description: undefined, + street_edge_id: undefined, + region_id: undefined, user_validation: undefined, tags: [] }; diff --git a/public/javascripts/Gallery/src/cards/CardContainer.js b/public/javascripts/Gallery/src/cards/CardContainer.js index 8e7dbd7a61..a4697242aa 100644 --- a/public/javascripts/Gallery/src/cards/CardContainer.js +++ b/public/javascripts/Gallery/src/cards/CardContainer.js @@ -450,6 +450,10 @@ function CardContainer(uiCardContainer) { return lastPage; } + function getModal() { + return modal; + } + self.fetchLabels = fetchLabels; self.getCards = getCards; self.getCurrentCards = getCurrentCards; @@ -465,6 +469,7 @@ function CardContainer(uiCardContainer) { self.getCardByIndex = getCardByIndex; self.getCurrentPage = getCurrentPage; self.getCurrentPageCards = getCurrentPageCards; + self.getModal = getModal; _init(); return this; diff --git a/public/javascripts/Gallery/src/modal/GalleryPanorama.js b/public/javascripts/Gallery/src/modal/GalleryPanorama.js index 8e78bdb05e..30043ec0e1 100644 --- a/public/javascripts/Gallery/src/modal/GalleryPanorama.js +++ b/public/javascripts/Gallery/src/modal/GalleryPanorama.js @@ -273,11 +273,47 @@ 195.93 / Math.pow(1.92, zoom); // parameters determined experimentally. } + /** + * Gets the current coordinates. + * @returns {{lng: float, lat: float}} + */ + function getCoords() { + let coords = self.panorama.getPosition(); + // Creates "TypeError: Cannot read properties of undefined (reading 'lat')", but still works fine. + return coords ? { 'lat' : coords.lat(), 'lng' : coords.lng() } : undefined; + } + + /** + * Get the current point of view. + * @returns {object} pov + */ + function getPov() { + var pov = self.panorama.getPov(); + + // Pov can be less than 0. So adjust it. + while (pov.heading < 0) { + pov.heading += 360; + } + + // Pov can be more than 360. Adjust it. + while (pov.heading > 360) { + pov.heading -= 360; + } + return pov; + } + + function getPanoId() { + return self.panoId + } + _init(); self.setPov = setPov; self.setPano = setPano; self.renderLabel = renderLabel; self.getOriginalPosition = getOriginalPosition; + self.getPosition = getCoords; + self.getPov = getPov; + self.getPanoId = getPanoId; return self; } diff --git a/public/javascripts/Gallery/src/modal/Modal.js b/public/javascripts/Gallery/src/modal/Modal.js index 02a079ec32..7ed0b1da37 100644 --- a/public/javascripts/Gallery/src/modal/Modal.js +++ b/public/javascripts/Gallery/src/modal/Modal.js @@ -51,6 +51,8 @@ function Modal(uiModal) { severity: undefined, temporary: undefined, description: undefined, + streetEdgeId: undefined, + regionId: undefined, user_validation: undefined, tags: [] }; @@ -128,15 +130,27 @@ function Modal(uiModal) { */ function populateModalDescriptionFields() { // Add timestamp data for when label was placed and when pano was created. - let labelTimestampData = document.createElement('div'); - labelTimestampData.className = 'label-timestamp'; - labelTimestampData.innerHTML = `
${i18next.t('labeled')}: ${moment(new Date(properties.label_timestamp)).format('LL, LT')}
`; + self.labelTimestampData = document.createElement('div'); + self.labelTimestampData.className = 'label-timestamp'; + self.labelTimestampData.innerHTML = `
${i18next.t('labeled')}: ${moment(new Date(properties.label_timestamp)).format('LL, LT')}
`; let panoTimestampData = document.createElement('div'); panoTimestampData.className = 'pano-timestamp'; panoTimestampData.innerHTML = `
${i18next.t('image-date')}: ${moment(properties.image_date).format('MMM YYYY')}
`; - self.timestamps.append(labelTimestampData); + self.timestamps.append(self.labelTimestampData); self.timestamps.append(panoTimestampData); + // Add info button to the right of the label timestamp. + let getPanoId = sg.modal().pano.getPanoId; + self.infoPopover = new GSVInfoPopover(self.labelTimestampData, sg.modal().pano.panorama, + sg.modal().pano.getPosition, getPanoId, + function () { return properties['street_edge_id']; }, function () { return properties['region_id']; }, + sg.modal().pano.getPov, false, + function() { sg.tracker.push('GSVInfoButton_Click', { panoId: getPanoId() }); }, + function() { sg.tracker.push('GSVInfoCopyToClipboard_Click', { panoId: getPanoId() }); }, + function() { sg.tracker.push('GSVInfoViewInGSV_Click', { panoId: getPanoId() }); }, + function () { return properties['label_id']; } + ); + // Add severity and tag display to the modal. new SeverityDisplay(self.severity, properties.severity, properties.label_type, true); new TagDisplay(self.tags, properties.tags, true); diff --git a/public/javascripts/SVLabel/css/svl-status.css b/public/javascripts/SVLabel/css/svl-status.css index b441a78b66..66fd525769 100644 --- a/public/javascripts/SVLabel/css/svl-status.css +++ b/public/javascripts/SVLabel/css/svl-status.css @@ -54,7 +54,7 @@ #accuracy-rating-tooltip { background-color:rgba(58,58,58, 0.9); color: white; - width: 400px; + width: 276px; top: 18px; border-radius: 15px; font-size: 10px; @@ -64,8 +64,9 @@ color: white; } -.popover-content { +#accuracy-rating-tooltip > .popover-content { padding: 6px 10px; + display: block; } #accuracy-rating-tooltip.top > .arrow { diff --git a/public/javascripts/SVLabel/css/svl.css b/public/javascripts/SVLabel/css/svl.css index dcace88c06..cf4a56a152 100644 --- a/public/javascripts/SVLabel/css/svl.css +++ b/public/javascripts/SVLabel/css/svl.css @@ -133,12 +133,33 @@ input[type="radio"] { } #svl-panorama-date-holder { + display: flex; + justify-content: flex-start; + height: 29px; position: absolute; - bottom: 3px; - left: 75px; + bottom: 0px; + left: 69px; z-index: 1; font-family: sans-serif; font-size: 15px; text-shadow: 0.5px 0.5px 0.5px rgba(0, 0, 0, 0.5); color: white; } + +#svl-panorama-date { + padding: 5px; +} + +/** + * Add this class to any text on the audit page that you want to be selectable. In audit.scala.html we set onselectstart + * to return false over the whole document, except elements where itself or a parent has this class. This was done to + * prevent an issue where dragging on the GSV pane would select a bunch of stuff in the background; issue link below. + * https://github.com/ProjectSidewalk/SidewalkWebpage/issues/121 + */ +.audit-selectable { } + +#gsv-info-button { + margin-left: 4px; + margin-top: 7px; + height: 16px; +} diff --git a/public/javascripts/SVLabel/src/SVLabel/Main.js b/public/javascripts/SVLabel/src/SVLabel/Main.js index 9707d8f55e..141f4b0e27 100644 --- a/public/javascripts/SVLabel/src/SVLabel/Main.js +++ b/public/javascripts/SVLabel/src/SVLabel/Main.js @@ -157,6 +157,13 @@ function Main (params) { svl.modalSkip = new ModalSkip(svl.form, svl.onboardingModel, svl.ribbon, svl.taskContainer, svl.tracker, svl.ui.leftColumn, svl.ui.modalSkip); svl.modalExample = new ModalExample(svl.modalModel, svl.onboardingModel, svl.ui.modalExample); + svl.infoPopover = new GSVInfoPopover(svl.ui.dateHolder, svl.panorama, svl.map.getPosition, svl.map.getPanoId, + svl.taskContainer.getCurrentTask().getStreetEdgeId, svl.taskContainer.getCurrentTask().getRegionId, + svl.map.getPov, true, function() { svl.tracker.push('GSVInfoButton_Click'); }, + function() { svl.tracker.push('GSVInfoCopyToClipboard_Click'); }, + function() { svl.tracker.push('GSVInfoViewInGSV_Click'); } + ); + // Survey for select users svl.surveyModalContainer = $("#survey-modal-container").get(0); @@ -426,6 +433,7 @@ function Main (params) { svl.ui.googleMaps = {}; svl.ui.googleMaps.holder = $("#google-maps-holder"); svl.ui.googleMaps.overlay = $("#google-maps-overlay"); + svl.ui.dateHolder = $("#svl-panorama-date-holder"); // Status holder svl.ui.status = {}; diff --git a/public/javascripts/SVLabel/src/SVLabel/task/Task.js b/public/javascripts/SVLabel/src/SVLabel/task/Task.js index f1b8115367..3232baf801 100644 --- a/public/javascripts/SVLabel/src/SVLabel/task/Task.js +++ b/public/javascripts/SVLabel/src/SVLabel/task/Task.js @@ -21,6 +21,7 @@ function Task (geojson, tutorialTask, currentLat, currentLng, startPointReversed var properties = { auditTaskId: null, streetEdgeId: null, + regionId: null, completedByAnyUser: null, priority: null, currentLat: currentLat, @@ -40,6 +41,7 @@ function Task (geojson, tutorialTask, currentLat, currentLng, startPointReversed _geojson = geojson; self.setProperty("streetEdgeId", _geojson.features[0].properties.street_edge_id); + self.setProperty("regionId", _geojson.features[0].properties.region_id); self.setProperty("completedByAnyUser", _geojson.features[0].properties.completed_by_any_user); self.setProperty("priority", _geojson.features[0].properties.priority); @@ -327,6 +329,10 @@ function Task (geojson, tutorialTask, currentLat, currentLng, startPointReversed return _geojson.features[0].properties.street_edge_id; }; + this.getRegionId = function () { + return _geojson.features[0].properties.region_id; + } + this.streetCompletedByAnyUser = function () { return properties.completedByAnyUser; }; diff --git a/public/javascripts/SVValidate/css/svv-panorama.css b/public/javascripts/SVValidate/css/svv-panorama.css index 41841d8ac2..109ead063c 100644 --- a/public/javascripts/SVValidate/css/svv-panorama.css +++ b/public/javascripts/SVValidate/css/svv-panorama.css @@ -14,16 +14,29 @@ } #svv-panorama-date-holder { + display: flex; + justify-content: flex-start; + height: 29px; position: absolute; - bottom: 3px; - left: 75px; - z-index: 1; + bottom: 0px; + left: 69px; + z-index: 2; font-family: sans-serif; font-size: 15px; text-shadow: 0.5px 0.5px 0.5px rgba(0, 0, 0, 0.5); color: white; } +#svv-panorama-date { + padding: 5px; +} + +#gsv-info-button { + margin-left: 4px; + margin-top: 7px; + height: 16px; +} + /* Black outline around GSV panorama. */ #svv-panorama-outline { border: 5px solid black; diff --git a/public/javascripts/SVValidate/src/Main.js b/public/javascripts/SVValidate/src/Main.js index 05a3abc067..dfb06d3409 100644 --- a/public/javascripts/SVValidate/src/Main.js +++ b/public/javascripts/SVValidate/src/Main.js @@ -129,6 +129,8 @@ function Main (param) { svv.ui.status.examples.popupImage = $("#example-image-popup"); svv.ui.status.examples.popupPointer = $("#example-image-popup-pointer"); svv.ui.status.examples.popupTitle = $("#example-image-popup-title"); + + svv.ui.dateHolder = $("#svv-panorama-date-holder"); } function _init() { @@ -145,7 +147,8 @@ function Main (param) { svv.statusExample = new StatusExample(svv.ui.status.examples); svv.tracker = new Tracker(); svv.labelDescriptionBox = new LabelDescriptionBox(); - svv.validationContainer = new ValidationContainer(param.labelList); + svv.labelContainer = new LabelContainer(); + svv.panoramaContainer = new PanoramaContainer(param.labelList); // There are certain features that will only make sense on desktop. if (!isMobile()) { @@ -168,6 +171,15 @@ function Main (param) { svv.modalInfo = new ModalInfo(svv.ui.modalInfo, param.modalText); svv.modalLandscape = new ModalLandscape(svv.ui.modalLandscape); svv.modalNoNewMission = new ModalNoNewMission(svv.ui.modalMission); + svv.infoPopover = new GSVInfoPopover(svv.ui.dateHolder, svv.panorama.getPanorama(), svv.panorama.getPosition, + svv.panorama.getPanoId, + function() { return svv.panoramaContainer.getCurrentLabel().getAuditProperty('streetEdgeId'); }, + function() { return svv.panoramaContainer.getCurrentLabel().getAuditProperty('regionId'); }, + svv.panorama.getPov, true, function() { svv.tracker.push('GSVInfoButton_Click'); }, + function() { svv.tracker.push('GSVInfoCopyToClipboard_Click'); }, + function() { svv.tracker.push('GSVInfoViewInGSV_Click'); }, + function() { return svv.panoramaContainer.getCurrentLabel().getAuditProperty('labelId'); } + ); svv.missionContainer = new MissionContainer(); svv.missionContainer.createAMission(param.mission, param.progress); diff --git a/public/javascripts/SVValidate/src/label/Label.js b/public/javascripts/SVValidate/src/label/Label.js index 0471b05abb..ab7a6938e9 100644 --- a/public/javascripts/SVValidate/src/label/Label.js +++ b/public/javascripts/SVValidate/src/label/Label.js @@ -23,6 +23,8 @@ function Label(params) { severity: undefined, temporary: undefined, description: undefined, + streetEdgeId: undefined, + regionId: undefined, tags: undefined, isMobile: undefined }; @@ -33,7 +35,6 @@ function Label(params) { canvasX: undefined, canvasY: undefined, endTimestamp: undefined, - labelId: undefined, heading: undefined, pitch: undefined, startTimestamp: undefined, @@ -83,22 +84,23 @@ function Label(params) { */ function _init() { if (params) { - if ("canvasHeight" in params) setAuditProperty("canvasHeight", params.canvasHeight); - if ("canvasWidth" in params) setAuditProperty("canvasWidth", params.canvasWidth); - if ("canvasX" in params) setAuditProperty("canvasX", params.canvasX); - if ("canvasY" in params) setAuditProperty("canvasY", params.canvasY); - if ("gsvPanoramaId" in params) setAuditProperty("gsvPanoramaId", params.gsvPanoramaId); - if ("imageDate" in params) setAuditProperty("imageDate", params.imageDate); - if ("labelTimestamp" in params) setAuditProperty("labelTimestamp", params.labelTimestamp); + if ("canvas_height" in params) setAuditProperty("canvasHeight", params.canvas_height); + if ("canvas_width" in params) setAuditProperty("canvasWidth", params.canvas_width); + if ("canvas_x" in params) setAuditProperty("canvasX", params.canvas_x); + if ("canvas_y" in params) setAuditProperty("canvasY", params.canvas_y); + if ("gsv_panorama_id" in params) setAuditProperty("gsvPanoramaId", params.gsv_panorama_id); + if ("image_date" in params) setAuditProperty("imageDate", params.image_date); + if ("label_timestamp" in params) setAuditProperty("labelTimestamp", params.label_timestamp); if ("heading" in params) setAuditProperty("heading", params.heading); - if ("labelId" in params) setAuditProperty("labelId", params.labelId); - if ("labelId" in params) setProperty("labelId", params.labelId); - if ("labelType" in params) setAuditProperty("labelType", params.labelType); + if ("label_id" in params) setAuditProperty("labelId", params.label_id); + if ("label_type" in params) setAuditProperty("labelType", params.label_type); if ("pitch" in params) setAuditProperty("pitch", params.pitch); if ("zoom" in params) setAuditProperty("zoom", params.zoom); if ("severity" in params) setAuditProperty("severity", params.severity); if ("temporary" in params) setAuditProperty("temporary", params.temporary); if ("description" in params) setAuditProperty("description", params.description); + if ("street_edge_id" in params) setAuditProperty("streetEdgeId", params.street_edge_id); + if ("region_id" in params) setAuditProperty("regionId", params.region_id); if ("tags" in params) setAuditProperty("tags", params.tags); setAuditProperty("isMobile", isMobile()); } @@ -219,6 +221,7 @@ function Label(params) { * NOTE: canvas_x and canvas_y are null when the label is not visible when validation occurs. * * @param validationResult Must be one of the following: {Agree, Disagree, Notsure}. + * @param comment An optional comment submitted with the validation. */ function validate(validationResult, comment) { // This is the POV of the PanoMarker, where the PanoMarker would be loaded at the center @@ -272,21 +275,21 @@ function Label(params) { case "Agree": setProperty("validationResult", 1); svv.missionContainer.getCurrentMission().updateValidationResult(1); - svv.labelContainer.push(getProperties()); + svv.labelContainer.push(getAuditProperty('labelId'), getProperties()); svv.missionContainer.updateAMission(); break; // Disagree option selected. case "Disagree": setProperty("validationResult", 2); svv.missionContainer.getCurrentMission().updateValidationResult(2); - svv.labelContainer.push(getProperties()); + svv.labelContainer.push(getAuditProperty('labelId'), getProperties()); svv.missionContainer.updateAMission(); break; // Not sure option selected. case "NotSure": setProperty("validationResult", 3); svv.missionContainer.getCurrentMission().updateValidationResult(3); - svv.labelContainer.push(getProperties()); + svv.labelContainer.push(getAuditProperty('labelId'), getProperties()); svv.missionContainer.updateAMission(); break; } diff --git a/public/javascripts/SVValidate/src/label/LabelContainer.js b/public/javascripts/SVValidate/src/label/LabelContainer.js index eccc2634f1..803947cc3d 100644 --- a/public/javascripts/SVValidate/src/label/LabelContainer.js +++ b/public/javascripts/SVValidate/src/label/LabelContainer.js @@ -18,9 +18,10 @@ function LabelContainer() { /** * Pushes a label to the list of current labels. + * @param labelId Integer label ID * @param labelMetadata Label metadata (validationProperties object) */ - function push(labelMetadata) { + function push(labelId, labelMetadata) { let data = { canvas_height: svv.canvasHeight, canvas_width: svv.canvasWidth, @@ -28,7 +29,7 @@ function LabelContainer() { canvas_y: labelMetadata.canvasY, end_timestamp: labelMetadata.endTimestamp, heading: labelMetadata.heading, - label_id: labelMetadata.labelId, + label_id: labelId, mission_id: svv.missionContainer.getCurrentMission().getProperty("missionId"), pitch: labelMetadata.pitch, start_timestamp: labelMetadata.startTimestamp, diff --git a/public/javascripts/SVValidate/src/mission/ValidationContainer.js b/public/javascripts/SVValidate/src/mission/ValidationContainer.js deleted file mode 100644 index 3720bbb445..0000000000 --- a/public/javascripts/SVValidate/src/mission/ValidationContainer.js +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Container holding the validation panorama. - * @param labelList Initial list of labels to validate for this mission - * @returns {ValidationContainer} - * @constructor - */ -function ValidationContainer (labelList) { - svv.panoramaContainer = undefined; - svv.labelContainer = undefined; - - let properties = { - agreeButtonList: undefined, - disagreeButtonList: undefined, - labelList: undefined, - notSureButtonList: undefined - }; - - let self = this; - - function _init() { - setProperty("labelList", labelList); - _createContainers(); - } - - /** - * Initializes the panoramaContainer and labelContainer. - * @private - */ - function _createContainers() { - svv.labelContainer = new LabelContainer(); - svv.panoramaContainer = new PanoramaContainer(labelList); - } - - /** - * Gets a specific validation property of this label. - * @param key Name of property. - * @returns Value associated with this key. - */ - function getProperty (key) { - return key in properties ? properties[key] : null; - } - - /** - * Sets the value of a single property in properties. - * @param key Name of property - * @param value Value to set property to. - */ - function setProperty(key, value) { - properties[key] = value; - return this; - } - - _init(); - - self.getProperty = getProperty; - self.setProperty = setProperty; - - return this; -} diff --git a/public/javascripts/SVValidate/src/panorama/Panorama.js b/public/javascripts/SVValidate/src/panorama/Panorama.js index 4f21dc2676..20b9127f30 100644 --- a/public/javascripts/SVValidate/src/panorama/Panorama.js +++ b/public/javascripts/SVValidate/src/panorama/Panorama.js @@ -206,7 +206,7 @@ function Panorama (label) { } } else { console.error("Error retrieving Panoramas: " + status); - svl.tracker.push("PanoId_NotFound", {'TargetPanoId': panoramaId}); + svv.tracker.push("PanoId_NotFound", {'TargetPanoId': panoramaId}); } }); } @@ -310,7 +310,7 @@ function Panorama (label) { * Skips the current label on this panorama and fetches a new label for validation. */ function skipLabel () { - svv.panoramaContainer.fetchNewLabel(currentLabel.getProperty('labelId')); + svv.panoramaContainer.fetchNewLabel(currentLabel.getAuditProperty('labelId')); } /** diff --git a/public/javascripts/SVValidate/src/panorama/PanoramaContainer.js b/public/javascripts/SVValidate/src/panorama/PanoramaContainer.js index 78c3923435..3497d26341 100644 --- a/public/javascripts/SVValidate/src/panorama/PanoramaContainer.js +++ b/public/javascripts/SVValidate/src/panorama/PanoramaContainer.js @@ -26,32 +26,6 @@ function PanoramaContainer (labelList) { svv.statusExample.updateLabelImage(labelList[0].getAuditProperty('labelType')); } - /** - * Uses label metadata to initialize a new label. - * @param metadata Metadata for the label. - * @returns {Label} Label object for this label. - * @private - */ - function _createSingleLabel (metadata) { - let labelMetadata = { - canvasHeight: metadata.canvas_height, - canvasWidth: metadata.canvas_width, - canvasX: metadata.canvas_x, - canvasY: metadata.canvas_y, - gsvPanoramaId: metadata.gsv_panorama_id, - heading: metadata.heading, - labelId: metadata.label_id, - labelType: metadata.label_type, - pitch: metadata.pitch, - zoom: metadata.zoom, - severity: metadata.severity, - temporary: metadata.temporary, - description: metadata.description, - tags: metadata.tags - }; - return new Label(labelMetadata); - } - /** * Fetches a single label from the database. When the user clicks skip, need to get more * because missions fetch exactly the number of labels that are needed to complete the mission. @@ -76,7 +50,7 @@ function PanoramaContainer (labelList) { data: JSON.stringify(data), dataType: 'json', success: function (labelMetadata) { - labels.push(_createSingleLabel(labelMetadata)); + labels.push(new Label(labelMetadata)); svv.missionContainer.updateAMissionSkip(); loadNewLabelOntoPanorama(svv.panorama); } @@ -108,6 +82,10 @@ function PanoramaContainer (labelList) { } } + function getCurrentLabel() { + return labels[getProperty('progress') - 1]; + } + /** * Resets the validation interface for a new mission. Loads a new set of label onto the panoramas. */ @@ -123,7 +101,7 @@ function PanoramaContainer (labelList) { */ function setLabelList (labelList) { Object.keys(labelList).map(function(key, index) { - labelList[key] = _createSingleLabel(labelList[key]); + labelList[key] = new Label(labelList[key]); }); labels = labelList; @@ -151,6 +129,7 @@ function PanoramaContainer (labelList) { self.fetchNewLabel = fetchNewLabel; self.getProperty = getProperty; self.loadNewLabelOntoPanorama = loadNewLabelOntoPanorama; + self.getCurrentLabel = getCurrentLabel; self.setProperty = setProperty; self.reset = reset; self.setLabelList = setLabelList; diff --git a/public/javascripts/common/GSVInfoPopover.js b/public/javascripts/common/GSVInfoPopover.js new file mode 100644 index 0000000000..4bdf1babca --- /dev/null +++ b/public/javascripts/common/GSVInfoPopover.js @@ -0,0 +1,202 @@ +/** + * Displays info about the current GSV pane. + * + * @param {HTMLElement} container Element where the info button will be displayed + * @param {StreetViewPanorama} panorama Panorama object + * @param {function} coords Function that returns current longitude and latitude coordinates + * @param {function} panoId Function that returns current panorama ID + * @param {function} streetEdgeId Function that returns current Street Edge ID + * @param {function} regionId Function that returns current Region ID + * @param {function} pov Function that returns current POV + * @param {Boolean} whiteIcon Set to true if using white icon, false if using blue icon. + * @param {function} infoLogging Function that adds the info button click to the appropriate logs. + * @param {function} clipboardLogging Function that adds the copy to clipboard click to the appropriate logs. + * @param {function} viewGSVLogging Function that adds the View in GSV click to the appropriate logs. + * @param {function} [labelId] Optional function that returns the Label ID. + * @returns {GSVInfoPopover} Popover object, which holds the popover title html, content html, info button html, and + * update values method + */ +function GSVInfoPopover (container, panorama, coords, panoId, streetEdgeId, regionId, pov, whiteIcon, infoLogging, clipboardLogging, viewGSVLogging, labelId) { + let self = this; + + function _init() { + // Create popover title bar. + self.titleBox = document.createElement('div'); + + let title = document.createElement('span'); + title.classList.add('popover-element'); + title.textContent = i18next.t('common:gsv-info.details-title'); + self.titleBox.appendChild(title); + + let clipboard = document.createElement('img'); + clipboard.classList.add('popover-element'); + clipboard.src = '/assets/images/icons/clipboard_copy.png'; + clipboard.id = 'clipboard'; + clipboard.setAttribute('data-toggle', 'popover'); + + self.titleBox.appendChild(clipboard); + + // Create popover content. + self.popoverContent = document.createElement('div'); + + // Add in container for each info type to the popover. + let dataList = document.createElement('ul'); + dataList.classList.add('list-group', 'list-group-flush', 'gsv-info-list-group'); + + addListElement('latitude', dataList); + addListElement('longitude', dataList); + addListElement('panorama-id', dataList); + addListElement('street-id', dataList); + addListElement('region-id', dataList); + if (labelId) addListElement('label-id', dataList); + + self.popoverContent.appendChild(dataList); + + // Create element for a link to GSV in a separate tab. + let linkGSV = document.createElement('a'); + linkGSV.classList.add('popover-element'); + linkGSV.id = 'gsv-link' + linkGSV.textContent = i18next.t('common:gsv-info.view-in-gsv'); + self.popoverContent.appendChild(linkGSV); + + // Create info button and add popover attributes. + self.infoButton = document.createElement('img'); + self.infoButton.classList.add('popover-element'); + self.infoButton.id = 'gsv-info-button'; + if (whiteIcon) self.infoButton.src = '/assets/images/icons/gsv_info_btn_white.svg'; + else self.infoButton.src = '/assets/images/icons/gsv_info_btn.png'; + self.infoButton.setAttribute('data-toggle', 'popover'); + + container.append(self.infoButton); + + // Enable popovers/tooltips and set options. + $('#gsv-info-button').popover({ + html: true, + placement: 'top', + container: 'body', + title: self.titleBox.innerHTML, + content: self.popoverContent.innerHTML + }).on('click', updateVals).on('shown.bs.popover', () => { + // Add popover-element classes to more elements, making it easier to dismiss popover on when outside it. + $('.popover-title').addClass('popover-element'); + $('.popover-content').addClass('popover-element'); + + // Initialize the popover for the clipboard. + $('#clipboard').popover({ + placement: 'top', + trigger: 'manual', + html: true, + content: `${i18next.t('common:gsv-info.copied-to-clipboard')}` + }); + }); + + // Dismiss popover when clicking outside it. Anything without the 'popover-element' class is considered outside. + $(document).on('mousedown', (e) => { + let tar = $(e.target); + if (!tar[0].classList.contains('popover-element')) { + $('#gsv-info-button').popover('hide'); + } + }); + // Dismiss popover whenever panorama changes. + panorama.addListener('pano_changed', () => { + $('#gsv-info-button').popover('hide'); + }) + } + + /** + * Update the values within the popover. + */ + function updateVals() { + // Log the click on the info button. + infoLogging(); + + // Get info values. + const currCoords = coords ? coords() : {lat: null, lng: null}; + const currPanoId = panoId ? panoId() : null; + const currStreetEdgeId = streetEdgeId ? streetEdgeId() : null; + const currRegionId = regionId ? regionId() : null; + const currPov = pov ? pov() : {heading: 0, pitch: 0}; + const currLabelId = labelId ? labelId() : null; + + function changeVals(key, val) { + if (!val) { + val = 'No Info'; + } else if (key === "latitude" || key === 'longitude') { + val = val.toFixed(8) + '°'; + } + let valSpan = document.getElementById(`${key}-value`); + valSpan.textContent = val; + } + changeVals('latitude', currCoords.lat); + changeVals('longitude', currCoords.lng); + changeVals('panorama-id', currPanoId); + changeVals('street-id', currStreetEdgeId); + changeVals('region-id', currRegionId); + if (currLabelId) changeVals('label-id', currLabelId); + + // Create GSV link and log the click. + let gsvLink = $('#gsv-link'); + gsvLink.attr('href', `https://www.google.com/maps/@?api=1&map_action=pano&viewpoint=${currCoords.lat}%2C${currCoords.lng}&heading=${currPov.heading}&pitch=${currPov.pitch}`); + gsvLink.attr('target', '_blank'); + gsvLink.on('click', viewGSVLogging); + + // Position popover. + let infoPopover = $('.popover'); + let infoRect = self.infoButton.getBoundingClientRect(); + let xpos = infoRect.x + (infoRect.width / 2) - (infoPopover.width() / 2); + infoPopover.css('left', `${xpos}px`); + + // Copy to clipboard. + $('#clipboard').on('click', function(e) { + // Log the click on the copy to keyboard button. + clipboardLogging(); + + let clipboardText = `${i18next.t(`common:gsv-info.latitude`)}: ${currCoords.lat}°\n` + + `${i18next.t(`common:gsv-info.longitude`)}: ${currCoords.lng}°\n` + + `${i18next.t(`common:gsv-info.panorama-id`)}: ${currPanoId}\n` + + `${i18next.t(`common:gsv-info.street-id`)}: ${currStreetEdgeId}\n` + + `${i18next.t(`common:gsv-info.region-id`)}: ${currRegionId}\n`; + if (currLabelId) clipboardText += `${i18next.t(`common:gsv-info.label-id`)}: ${currLabelId}`; + navigator.clipboard.writeText(clipboardText); + + // The clipboard popover will only show one time until you close and reopen the info button popover. I have + // no idea why that's happening, but for some reason it works if you put it in a setTimeout. So I have a one + // ms delay before showing the popover. Then it disappears after 1.5 seconds. + setTimeout(function() { + $(e.target).popover('show'); + setTimeout(function() { + $(e.target).popover('hide'); + }, 1500); + }, 1); + }); + } + + /** + * Creates a key-value pair display within the popover. + * @param {String} key Key name of the key-value pair + * @param {HTMLElement} dataList List element container to add list item to + */ + function addListElement(key, dataList) { + let listElement = document.createElement('li'); + listElement.classList.add('list-group-item', 'info-list-item', 'popover-element', 'audit-selectable'); + + let keySpan = document.createElement('span'); + keySpan.classList.add('info-key', 'popover-element'); + keySpan.textContent = i18next.t(`common:gsv-info.${key}`); + listElement.appendChild(keySpan); + + let valSpan = document.createElement('span'); + valSpan.classList.add('info-val', 'popover-element'); + valSpan.textContent = '-'; + valSpan.id = `${key}-value` + + listElement.appendChild(valSpan); + dataList.appendChild(listElement); + } + + _init(); + + self.updateVals = updateVals; + + return self; +} diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 5c9ce39625..5555bd112a 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -103,5 +103,16 @@ "APS": "APS", "missing crosswalk": "missing crosswalk", "no bus stop access": "no bus stop access" + }, + "gsv-info": { + "details-title": "Details", + "latitude": "Latitude", + "longitude": "Longitude", + "panorama-id": "Panorama ID", + "street-id": "Street ID", + "region-id": "Region ID", + "label-id": "Label ID", + "view-in-gsv": "View in Google Street View", + "copied-to-clipboard": "Data copied to your clipboard!" } } diff --git a/public/locales/es/common.json b/public/locales/es/common.json index 88e6684f31..18039b8774 100644 --- a/public/locales/es/common.json +++ b/public/locales/es/common.json @@ -103,5 +103,16 @@ "APS": "APS", "missing crosswalk": "paso peatonal ausente (i)", "no bus stop access": "No hay acceso a la parada de autobús" + }, + "gsv-info": { + "details-title": "Detalles", + "latitude": "Latitud", + "longitude": "Longitud", + "panorama-id": "ID de panorama", + "street-id": "ID de calle", + "region-id": "ID de región", + "label-id": "ID de etiqueta", + "view-in-gsv": "Ver en Google Street View", + "copied-to-clipboard": "¡Datos copiados a su portapapeles!" } } diff --git a/public/locales/nl/common.json b/public/locales/nl/common.json index 540eefac6b..987e4b9df1 100644 --- a/public/locales/nl/common.json +++ b/public/locales/nl/common.json @@ -103,5 +103,16 @@ "APS": "APS", "missing crosswalk": "missend zebrapad", "no bus stop access": "geen bushalte toegang" + }, + "gsv-info": { + "details-title": "Details", + "latitude": "Breedtegraad", + "longitude": "Lengtegraad", + "panorama-id": "Panorama-ID", + "street-id": "Straat-ID", + "region-id": "Regio-ID", + "label-id": "Label-ID", + "view-in-gsv": "Bekijken in Google Streetview", + "copied-to-clipboard": "Gegevens gekopieerd naar uw klembord!" } } diff --git a/public/stylesheets/main.css b/public/stylesheets/main.css index 972c9230a4..3eef6a212b 100644 --- a/public/stylesheets/main.css +++ b/public/stylesheets/main.css @@ -720,6 +720,54 @@ p#conditional-text { text-decoration:underline; } +/* GSV Info Popover styling. */ +.info-key { + font-weight: bold; + margin-right: 20px; +} + +.info-list-item { + display: flex; + justify-content: space-between; +} + +.popover { + max-width: 400px; +} + +.popover-title { + display: flex; + justify-content: space-between; + height: 36px; +} + +.popover-content { + display: flex; + flex-direction: column; + justify-content: center; +} + +#gsv-link { + text-align: center; + width: fit-content; + margin-left: auto; + margin-right: auto; +} + +#clipboard { + max-height: 100%; + cursor: pointer; +} + +#clipboard:active { + opacity: 50%; +} + +.clipboard-tooltip { + font-size: 12px; + white-space: nowrap; +} + @media only screen and (max-device-width: 500px) { #awardnum { width: 100% !important; @@ -1204,6 +1252,14 @@ kbd { vertical-align: middle; } +#gsv-info-button { + cursor: pointer; +} + +.gsv-info-list-group { + margin-bottom: 8px; +} + .mobile-popover { background-color:rgba(58,58,58, 0.9); color: white;