diff --git a/app/formats/json/ExploreFormats.scala b/app/formats/json/ExploreFormats.scala index f4d0b37f42..4b50f87d04 100644 --- a/app/formats/json/ExploreFormats.scala +++ b/app/formats/json/ExploreFormats.scala @@ -47,6 +47,8 @@ object ExploreFormats { zoom: Int, lat: Option[Float], lng: Option[Float], + latUsingGsv: Option[Float], + lngUsingGsv: Option[Float], computationMethod: Option[String] ) case class LabelSubmission( @@ -237,6 +239,8 @@ object ExploreFormats { (JsPath \ "zoom").read[Int] and (JsPath \ "lat").readNullable[Float] and (JsPath \ "lng").readNullable[Float] and + (JsPath \ "lat_using_gsv").readNullable[Float] and + (JsPath \ "lng_using_gsv").readNullable[Float] and (JsPath \ "computation_method").readNullable[String] )(LabelPointSubmission.apply _) diff --git a/app/formats/json/LabelFormats.scala b/app/formats/json/LabelFormats.scala index 674fb7f151..5bc172ecf9 100644 --- a/app/formats/json/LabelFormats.scala +++ b/app/formats/json/LabelFormats.scala @@ -184,12 +184,13 @@ object LabelFormats { def resumeLabelMetadatatoJson(label: ResumeLabelMetadata, allTags: Seq[Tag]): JsObject = { Json.obj( - "labelId" -> label.labelData.labelId, - "labelType" -> label.labelType, - "panoId" -> label.labelData.gsvPanoramaId, - "panoLat" -> label.panoLat, - "panoLng" -> label.panoLng, - "originalPov" -> Json.obj( + "labelId" -> label.labelData.labelId, + "labelType" -> label.labelType, + "panoCaptureDate" -> label.panoCaptureDate, + "panoId" -> label.labelData.gsvPanoramaId, + "panoLat" -> label.panoLat, + "panoLng" -> label.panoLng, + "originalPov" -> Json.obj( "heading" -> label.pointData.heading, "pitch" -> label.pointData.pitch, "zoom" -> label.pointData.zoom @@ -216,7 +217,9 @@ object LabelFormats { "auditTaskId" -> label.labelData.auditTaskId, "missionId" -> label.labelData.missionId, "labelLat" -> label.pointData.lat, - "labelLng" -> label.pointData.lng + "labelLng" -> label.pointData.lng, + "latUsingGsv" -> label.pointData.geomUsingGsv.map(_.getY), + "lngUsingGsv" -> label.pointData.geomUsingGsv.map(_.getX) ) } } diff --git a/app/models/label/LabelPointTable.scala b/app/models/label/LabelPointTable.scala index 9ee8bd3c8a..a47dd5674f 100644 --- a/app/models/label/LabelPointTable.scala +++ b/app/models/label/LabelPointTable.scala @@ -21,6 +21,7 @@ case class LabelPoint( lat: Option[Float], lng: Option[Float], geom: Option[Point], + geomUsingGsv: Option[Point], computationMethod: Option[String] ) @@ -37,9 +38,10 @@ class LabelPointTableDef(tag: slick.lifted.Tag) extends Table[LabelPoint](tag, " def lat: Rep[Option[Float]] = column[Option[Float]]("lat") def lng: Rep[Option[Float]] = column[Option[Float]]("lng") def geom: Rep[Option[Point]] = column[Option[Point]]("geom") + def geomUsingGsv: Rep[Option[Point]] = column[Option[Point]]("geom_using_gsv") def computationMethod: Rep[Option[String]] = column[Option[String]]("computation_method") - def * = (labelPointId, labelId, panoX, panoY, canvasX, canvasY, heading, pitch, zoom, lat, lng, geom, + def * = (labelPointId, labelId, panoX, panoY, canvasX, canvasY, heading, pitch, zoom, lat, lng, geom, geomUsingGsv, computationMethod) <> ((LabelPoint.apply _).tupled, LabelPoint.unapply) // def label: ForeignKeyQuery[LabelTable, Label] = diff --git a/app/models/label/LabelTable.scala b/app/models/label/LabelTable.scala index a9226bac82..d9a51d0f69 100644 --- a/app/models/label/LabelTable.scala +++ b/app/models/label/LabelTable.scala @@ -147,6 +147,7 @@ case class ResumeLabelMetadata( labelData: Label, labelType: String, pointData: LabelPoint, + panoCaptureDate: String, panoLat: Option[Float], panoLng: Option[Float], cameraHeading: Option[Float], @@ -1218,8 +1219,9 @@ class LabelTable @Inject() (protected val dbConfigProvider: DatabaseConfigProvid _gsvData <- gsvData if _label.gsvPanoramaId === _gsvData.gsvPanoramaId if _mission.regionId === regionId && _mission.userId === userId if _labelPoint.lat.isDefined && _labelPoint.lng.isDefined - } yield (_label, _labelType.labelType, _labelPoint, _gsvData.lat, _gsvData.lng, _gsvData.cameraHeading, - _gsvData.cameraPitch, _gsvData.width, _gsvData.height)).result.map(_.map(ResumeLabelMetadata.tupled)) + } yield (_label, _labelType.labelType, _labelPoint, _gsvData.captureDate, _gsvData.lat, _gsvData.lng, + _gsvData.cameraHeading, _gsvData.cameraPitch, _gsvData.width, _gsvData.height)).result + .map(_.map(ResumeLabelMetadata.tupled)) } /** diff --git a/app/service/ExploreService.scala b/app/service/ExploreService.scala index b69ba8cfa8..db85a2d423 100644 --- a/app/service/ExploreService.scala +++ b/app/service/ExploreService.scala @@ -369,6 +369,10 @@ class ExploreServiceImpl @Inject() ( _lat <- point.lat _lng <- point.lng } yield gf.createPoint(new Coordinate(_lng.toDouble, _lat.toDouble)) + val pointGeomUsingGsv: Option[Point] = for { + _lat <- point.latUsingGsv + _lng <- point.lngUsingGsv + } yield gf.createPoint(new Coordinate(_lng.toDouble, _lat.toDouble)) for { // Use label's lat/lng to determine street_edge_id. If lat/lng isn't defined, use audit_task's as backup. @@ -406,7 +410,7 @@ class ExploreServiceImpl @Inject() ( // Add an entry to the label_point table. _ <- labelPointTable.insert( LabelPoint(0, newLabelId, point.panoX, point.panoY, point.canvasX, point.canvasY, point.heading, point.pitch, - point.zoom, point.lat, point.lng, pointGeom, point.computationMethod) + point.zoom, point.lat, point.lng, pointGeom, pointGeomUsingGsv, point.computationMethod) ) } yield { (newLabelId, label.temporaryLabelId, timeCreated) diff --git a/conf/evolutions/default/278.sql b/conf/evolutions/default/278.sql new file mode 100644 index 0000000000..cbd969ae83 --- /dev/null +++ b/conf/evolutions/default/278.sql @@ -0,0 +1,7 @@ +# --- !Ups +ALTER TABLE label_point + ADD COLUMN geom_using_gsv geometry(Point, 4326); + +# --- !Downs +ALTER TABLE label_point + DROP COLUMN geom_using_gsv; diff --git a/public/javascripts/SVLabel/src/SVLabel/Main.js b/public/javascripts/SVLabel/src/SVLabel/Main.js index 3fdf625cd6..e75d84dd5f 100644 --- a/public/javascripts/SVLabel/src/SVLabel/Main.js +++ b/public/javascripts/SVLabel/src/SVLabel/Main.js @@ -148,6 +148,13 @@ function Main (params) { }); logPageFocus(); + // Sets up Google Maps OverlayView, which is used to tie labels to a lat/lng, showing them on nearby panos. + svl.overlay = new google.maps.OverlayView(); + svl.overlay.onAdd = function () {}; + svl.overlay.draw = function () {}; + svl.overlay.onRemove = function () {}; + svl.overlay.setMap(svl.panorama); + // Modals var modalMissionCompleteMap = new ModalMissionCompleteMap(svl.ui.modalMissionComplete, params.mapboxApiKey); var modalMissionCompleteProgressBar = new ModalMissionCompleteProgressBar(svl.ui.modalMissionComplete); @@ -320,7 +327,7 @@ function Main (params) { svl.labelContainer.fetchLabelsToResumeMission(neighborhood.getRegionId(), function (result) { svl.statusFieldNeighborhood.setLabelCount(svl.labelContainer.countLabels()); - svl.canvas.setOnlyLabelsOnPanoAsVisible(svl.map.getPanoId()); + svl.canvas.setOnlyLabelsInViewAsVisible(svl.map.getPanoId()); // Count the labels of each label type to initialize the current mission label counts. var counter = {'CurbRamp': 0, 'NoCurbRamp': 0, 'Obstacle': 0, 'SurfaceProblem': 0, 'NoSidewalk': 0, 'Other': 0}; diff --git a/public/javascripts/SVLabel/src/SVLabel/canvas/Canvas.js b/public/javascripts/SVLabel/src/SVLabel/canvas/Canvas.js index 1293be000f..ed19849e6b 100644 --- a/public/javascripts/SVLabel/src/SVLabel/canvas/Canvas.js +++ b/public/javascripts/SVLabel/src/SVLabel/canvas/Canvas.js @@ -142,7 +142,7 @@ function Canvas(ribbon) { if (!status.disableLabeling && currTime - mouseStatus.prevMouseUpTime > 300) { createLabel(mouseStatus.leftUpX, mouseStatus.leftUpY); clear(); - setOnlyLabelsOnPanoAsVisible(svl.map.getPanoId()); + setOnlyLabelsInViewAsVisible(svl.map.getPanoId()); render(); } @@ -330,10 +330,10 @@ function Canvas(ribbon) { /** * Sets labels on the given pano as visible, all others as hidden. */ - function setOnlyLabelsOnPanoAsVisible(panoramaId) { + function setOnlyLabelsInViewAsVisible(panoramaId) { var labels = svl.labelContainer.getCanvasLabels(); for (var i = 0; i < labels.length; i += 1) { - if (labels[i].getPanoId() === panoramaId && !labels[i].isDeleted()) { + if (!labels[i].isDeleted()) { labels[i].setVisibility('visible'); } else { labels[i].setVisibility('hidden'); @@ -376,7 +376,7 @@ function Canvas(ribbon) { self.setStatus = setStatus; self.showLabelHoverInfo = showLabelHoverInfo; self.setVisibility = setVisibility; - self.setOnlyLabelsOnPanoAsVisible = setOnlyLabelsOnPanoAsVisible; + self.setOnlyLabelsInViewAsVisible = setOnlyLabelsInViewAsVisible; self.unlockDisableLabelDelete = unlockDisableLabelDelete; self.saveGSVScreenshot = saveGSVScreenshot; diff --git a/public/javascripts/SVLabel/src/SVLabel/data/Form.js b/public/javascripts/SVLabel/src/SVLabel/data/Form.js index 4e14a38368..8d9708ee2b 100644 --- a/public/javascripts/SVLabel/src/SVLabel/data/Form.js +++ b/public/javascripts/SVLabel/src/SVLabel/data/Form.js @@ -99,6 +99,9 @@ function Form (labelContainer, missionModel, missionContainer, navigationModel, var tempLabelId = label.getProperty('temporaryLabelId'); var auditTaskId = label.getProperty('auditTaskId'); + var labelLatUsingGsv = label.getProperty('latUsingGsv'); + var labelLngUsingGsv = label.getProperty('lngUsingGsv'); + // If this label is a new label, get the timestamp of its creation from the corresponding interaction. var associatedInteraction = data.interactions.find(interaction => interaction.action === 'LabelingCanvas_FinishLabeling' @@ -127,7 +130,9 @@ function Form (labelContainer, missionModel, missionContainer, navigationModel, pitch: prop.originalPov.pitch, zoom : prop.originalPov.zoom, lat : null, - lng : null + lng : null, + lat_using_gsv : null, + lng_using_gsv : null } }; @@ -137,6 +142,11 @@ function Form (labelContainer, missionModel, missionContainer, navigationModel, temp.label_point.computation_method = labelLatLng.latLngComputationMethod; } + if (labelLatUsingGsv && labelLngUsingGsv) { + temp.label_point.lat_using_gsv = labelLatUsingGsv; + temp.label_point.lng_using_gsv = labelLngUsingGsv; + } + data.labels.push(temp) } diff --git a/public/javascripts/SVLabel/src/SVLabel/label/Label.js b/public/javascripts/SVLabel/src/SVLabel/label/Label.js index 50141e7632..20e0fdcf21 100644 --- a/public/javascripts/SVLabel/src/SVLabel/label/Label.js +++ b/public/javascripts/SVLabel/src/SVLabel/label/Label.js @@ -51,10 +51,13 @@ function Label(params) { povOfLabelIfCentered: undefined, labelLat: undefined, labelLng: undefined, + latUsingGsv: undefined, + lngUsingGsv: undefined, latLngComputationMethod: undefined, panoId: undefined, panoLat: undefined, panoLng: undefined, + panoCaptureDate: undefined, cameraHeading: undefined, panoWidth: undefined, panoHeight: undefined, @@ -94,6 +97,7 @@ function Label(params) { properties.panoXY = util.panomarker.calculatePanoXYFromPov( properties.povOfLabelIfCentered, properties.cameraHeading, properties.panoWidth, properties.panoHeight ); + properties.panoCaptureDate = panoData.imageDate; } // Create the marker on the minimap. @@ -203,9 +207,36 @@ function Label(params) { // Update the coordinates of the label on the canvas. if (svl.map.getPovChangeStatus()) { - properties.currCanvasXY = util.panomarker.getCanvasCoordinate( - properties.povOfLabelIfCentered, pov, util.EXPLORE_CANVAS_WIDTH, util.EXPLORE_CANVAS_HEIGHT, svl.LABEL_ICON_RADIUS - ); + if (svl.map.getPanoId() === properties.panoId) { + properties.currCanvasXY = util.panomarker.getCanvasCoordinate( + properties.povOfLabelIfCentered, pov, util.EXPLORE_CANVAS_WIDTH, util.EXPLORE_CANVAS_HEIGHT, svl.LABEL_ICON_RADIUS + ); + } else { + // If the pano has changed, we need to recalculate the label's canvas coordinates. Using the label's + // lat/lng. Ideally use the one calculated from GSV, but use our own fallback lat/lng if not avail. + let latLng = null; + if (getProperty('latUsingGsv') && getProperty('lngUsingGsv')) { + latLng = new google.maps.LatLng(getProperty('latUsingGsv'), getProperty('lngUsingGsv')); + } else if (getProperty('labelLat') && getProperty('labelLng')) { + // Fallback to regression-calculated lat/lng if GSV lat/lng is not available. + latLng = this.toLatLng(); + } + + // Calculate new canvas coordinates from the label's lat/lng. + const projection = svl.overlay.getProjection(); + const canvasXY = projection.fromLatLngToContainerPixel(latLng); + if(canvasXY != null) { + properties.currCanvasXY = { + x: canvasXY.x, + y: canvasXY.y + }; + } else { + properties.currCanvasXY = { + x: null, + y: null + }; + } + } } // Draw the label icon if it's in the visible part of the pano. @@ -353,7 +384,32 @@ function Label(params) { */ function toLatLng() { if (!properties.labelLat) { - // Estimate the latlng point from the camera position and the heading when point cloud data isn't available. + // Estimating lat/lng from GSV canvas coordinates. + let gsvEstimatedLatLng = null; + if (properties.currCanvasXY.x) { + try { + const projection = svl.overlay.getProjection(); + + const latLng = projection.fromContainerPixelToLatLng({ + x: properties.currCanvasXY.x, + y: properties.currCanvasXY.y + }); + + gsvEstimatedLatLng = {lat: latLng.lat(), lng: latLng.lng()} + } catch (e) { + console.error('Error estimating GSV lat/lng for label:', e); + } + } + if (gsvEstimatedLatLng) { + const gsvLatLng = { + lat: gsvEstimatedLatLng.lat, + lng: gsvEstimatedLatLng.lng, + }; + setProperty('latUsingGsv', gsvLatLng.lat); + setProperty('lngUsingGsv', gsvLatLng.lng); + } + + // Estimate the latlng point from the camera position and the heading. var panoLat = getProperty("panoLat"); var panoLng = getProperty("panoLng"); var panoHeading = getProperty("originalPov").heading; diff --git a/public/javascripts/SVLabel/src/SVLabel/label/LabelContainer.js b/public/javascripts/SVLabel/src/SVLabel/label/LabelContainer.js index bdc54f6bcc..42a10376d0 100644 --- a/public/javascripts/SVLabel/src/SVLabel/label/LabelContainer.js +++ b/public/javascripts/SVLabel/src/SVLabel/label/LabelContainer.js @@ -105,11 +105,31 @@ function LabelContainer ($, nextTemporaryLabelId) { } /** - * Returns labels for the current pano ID. + * Returns labels for the current view. */ this.getCanvasLabels = function () { - let panoId = svl.map.getPanoId(); - return allLabels[panoId] ? allLabels[panoId] : []; + const panoId = svl.map.getPanoId(); + const panoLatLng = svl.map.getPosition(); + // During the transition to the next panorama, the new panorama might not be in the container. + // Therefore, we must check if the panorama exists before proceeding. + if (svl.panoramaContainer.getPanorama(panoId) === null) { + return []; + } + const panoDate = svl.panoramaContainer.getPanorama(panoId).data().imageDate; + return this.getAllLabels().filter(function (label) { + // Is the label in the current pano? + const presentInPano = label.getPanoId() === svl.map.getPanoId(); + // Calculate distance between label and current pano. + const labelLatLng = label.toLatLng(); + const distance = turf.distance(turf.point([labelLatLng.lng, labelLatLng.lat]), + turf.point([panoLatLng.lng, panoLatLng.lat]), { units: "meters" }); + // Check if date of label and pano match. + const dateMatches = panoDate === label.getProperty('panoCaptureDate'); + + // We may want to do further testing to determine the optimal distance threshold. + // For now, we use 25m as a threshold. + return presentInPano || (distance < 25 && dateMatches); + }); }; /** diff --git a/public/javascripts/SVLabel/src/SVLabel/navigation/MapService.js b/public/javascripts/SVLabel/src/SVLabel/navigation/MapService.js index 0ab979218c..1c63d045eb 100644 --- a/public/javascripts/SVLabel/src/SVLabel/navigation/MapService.js +++ b/public/javascripts/SVLabel/src/SVLabel/navigation/MapService.js @@ -615,7 +615,7 @@ function MapService (canvas, neighborhoodModel, uiMap, params) { povChange["status"] = true; _canvas.clear(); - _canvas.setOnlyLabelsOnPanoAsVisible(panoId); + _canvas.setOnlyLabelsInViewAsVisible(panoId); _canvas.render(); povChange["status"] = false; @@ -1115,7 +1115,7 @@ function MapService (canvas, neighborhoodModel, uiMap, params) { function updateCanvas() { _canvas.clear(); if (status.currPanoId !== getPanoId()) { - _canvas.setOnlyLabelsOnPanoAsVisible(getPanoId()); + _canvas.setOnlyLabelsInViewAsVisible(getPanoId()); } status.currPanoId = getPanoId(); _canvas.render();