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;