From a7d41aac23d75ffca1e99ab50a7891aa4a23621a Mon Sep 17 00:00:00 2001 From: Florian M Date: Thu, 5 Jun 2025 15:40:44 +0200 Subject: [PATCH 1/7] use objectid in tracingstore where applicable --- .../AuthenticationController.scala | 2 +- .../WKRemoteTracingStoreController.scala | 3 +- app/models/annotation/AnnotationService.scala | 2 +- .../AccessibleBySwitchingService.scala | 18 ++- build.sbt | 10 +- conf/webknossos.latest.routes | 2 +- .../models/annotation/AnnotationSource.scala | 3 +- .../services/AccessTokenService.scala | 9 +- .../TSRemoteDatastoreClient.scala | 7 +- .../TSRemoteWebknossosClient.scala | 27 +++-- .../annotation/AnnotationReversion.scala | 9 +- .../AnnotationTransactionService.scala | 49 ++++---- .../annotation/TSAnnotationService.scala | 111 +++++++++--------- .../EditableMappingController.scala | 5 +- .../SkeletonTracingController.scala | 3 +- .../controllers/TSAnnotationController.scala | 34 ++---- .../controllers/VolumeTracingController.scala | 13 +- ...VolumeTracingZarrStreamingController.scala | 3 +- .../tracings/RemoteFallbackLayer.scala | 3 +- .../tracings/TemporaryTracingService.scala | 15 +-- .../EditableMappingLayer.scala | 7 +- .../EditableMappingMergeService.scala | 17 +-- .../EditableMappingUpdater.scala | 3 +- .../tracings/volume/TSFullMeshService.scala | 13 +- .../VolumeSegmentStatisticsService.scala | 9 +- .../tracings/volume/VolumeTracingLayer.scala | 3 +- .../volume/VolumeTracingService.scala | 45 +++---- ...alableminds.webknossos.tracingstore.routes | 34 +++--- 28 files changed, 238 insertions(+), 221 deletions(-) diff --git a/app/controllers/AuthenticationController.scala b/app/controllers/AuthenticationController.scala index b0da06d0b47..71f3e7bc693 100755 --- a/app/controllers/AuthenticationController.scala +++ b/app/controllers/AuthenticationController.scala @@ -215,7 +215,7 @@ class AuthenticationController @Inject()( } yield result def accessibleBySwitching(datasetId: Option[ObjectId], - annotationId: Option[String], + annotationId: Option[ObjectId], workflowHash: Option[String]): Action[AnyContent] = sil.SecuredAction.async { implicit request => for { diff --git a/app/controllers/WKRemoteTracingStoreController.scala b/app/controllers/WKRemoteTracingStoreController.scala index d01693aaa2f..ae8c65c1481 100644 --- a/app/controllers/WKRemoteTracingStoreController.scala +++ b/app/controllers/WKRemoteTracingStoreController.scala @@ -82,8 +82,7 @@ class WKRemoteTracingStoreController @Inject()(tracingStoreService: TracingStore tracingStoreService.validateAccess(name, key) { _ => val report = request.body for { - annotationId <- ObjectId.fromString(report.annotationId) - annotation <- annotationDAO.findOne(annotationId) + annotation <- annotationDAO.findOne(report.annotationId) _ <- ensureAnnotationNotFinished(annotation) _ <- annotationDAO.updateModified(annotation._id, Instant.now) _ = report.statistics.map(statistics => annotationService.updateStatistics(annotation._id, statistics)) diff --git a/app/models/annotation/AnnotationService.scala b/app/models/annotation/AnnotationService.scala index 00f4e528372..7f0a4ee2480 100755 --- a/app/models/annotation/AnnotationService.scala +++ b/app/models/annotation/AnnotationService.scala @@ -795,7 +795,7 @@ class AnnotationService @Inject()( dataStore <- dataStoreDAO.findOneByName(dataset._dataStore.trim) ?~> "datastore.notFound" tracingStore <- tracingStoreDAO.findFirst annotationSource = AnnotationSource( - id = annotation.id, + id = annotation._id, annotationLayers = annotation.annotationLayers, datasetDirectoryName = dataset.directoryName, organizationId = organization._id, diff --git a/app/security/AccessibleBySwitchingService.scala b/app/security/AccessibleBySwitchingService.scala index babcffdf5df..4aa6db15626 100644 --- a/app/security/AccessibleBySwitchingService.scala +++ b/app/security/AccessibleBySwitchingService.scala @@ -30,7 +30,7 @@ class AccessibleBySwitchingService @Inject()( def getOrganizationToSwitchTo(user: User, datasetId: Option[ObjectId], - annotationId: Option[String], + annotationId: Option[ObjectId], workflowHash: Option[String])(implicit ctx: DBAccessContext): Fox[Organization] = for { isSuperUser <- multiUserDAO.findOne(user._multiUser).map(_.isSuperUser) @@ -42,7 +42,7 @@ class AccessibleBySwitchingService @Inject()( } yield selectedOrganization private def accessibleBySwitchingForSuperUser(datasetIdOpt: Option[ObjectId], - annotationIdOpt: Option[String], + annotationIdOpt: Option[ObjectId], workflowHashOpt: Option[String]): Fox[Organization] = { implicit val ctx: DBAccessContext = GlobalAccessContext (datasetIdOpt, annotationIdOpt, workflowHashOpt) match { @@ -53,8 +53,7 @@ class AccessibleBySwitchingService @Inject()( } yield organization case (None, Some(annotationId), None) => for { - annotationObjectId <- ObjectId.fromString(annotationId) - annotation <- annotationDAO.findOne(annotationObjectId) // Note: this does not work for compound annotations. + annotation <- annotationDAO.findOne(annotationId) // Note: this does not work for compound annotations. user <- userDAO.findOne(annotation._user) organization <- organizationDAO.findOne(user._organization) } yield organization @@ -69,7 +68,7 @@ class AccessibleBySwitchingService @Inject()( private def accessibleBySwitchingForMultiUser(multiUserId: ObjectId, datasetIdOpt: Option[ObjectId], - annotationIdOpt: Option[String], + annotationIdOpt: Option[ObjectId], workflowHashOpt: Option[String]): Fox[Organization] = for { identities <- userDAO.findAllByMultiUser(multiUserId) @@ -80,7 +79,7 @@ class AccessibleBySwitchingService @Inject()( private def canAccessDatasetOrAnnotationOrWorkflow(user: User, datasetIdOpt: Option[ObjectId], - annotationIdOpt: Option[String], + annotationIdOpt: Option[ObjectId], workflowHashOpt: Option[String]): Fox[Boolean] = { val ctx = AuthorizedAccessContext(user) (datasetIdOpt, annotationIdOpt, workflowHashOpt) match { @@ -99,12 +98,11 @@ class AccessibleBySwitchingService @Inject()( foundFox.shiftBox.map(_.isDefined) } - private def canAccessAnnotation(user: User, ctx: DBAccessContext, annotationId: String): Fox[Boolean] = { + private def canAccessAnnotation(user: User, ctx: DBAccessContext, annotationId: ObjectId): Fox[Boolean] = { val foundFox = for { - annotationIdParsed <- ObjectId.fromString(annotationId) - annotation <- annotationDAO.findOne(annotationIdParsed)(GlobalAccessContext) + annotation <- annotationDAO.findOne(annotationId)(GlobalAccessContext) _ <- Fox.fromBool(annotation.state != Cancelled) - restrictions <- annotationProvider.restrictionsFor(AnnotationIdentifier(annotation.typ, annotationIdParsed))(ctx) + restrictions <- annotationProvider.restrictionsFor(AnnotationIdentifier(annotation.typ, annotationId))(ctx) _ <- restrictions.allowAccess(user) } yield () foundFox.shiftBox.map(_.isDefined) diff --git a/build.sbt b/build.sbt index 18bf21caa65..4eb1bc89d72 100644 --- a/build.sbt +++ b/build.sbt @@ -34,13 +34,10 @@ Compile / console / scalacOptions -= "-Xlint:unused" scapegoatIgnoredFiles := Seq(".*/Tables.scala", ".*/Routes.scala", ".*/.*mail.*template\\.scala") scapegoatDisabledInspections := Seq("FinalModifierOnCaseClass", "UnusedMethodParameter", "UnsafeTraversableMethods") -// Allow path binding for ObjectId -routesImport += "com.scalableminds.util.objectid.ObjectId" - lazy val commonSettings = Seq( resolvers ++= DependencyResolvers.dependencyResolvers, Compile / doc / sources := Seq.empty, - Compile / packageDoc / publishArtifact := false + Compile / packageDoc / publishArtifact := false, ) lazy val protocolBufferSettings = Seq( @@ -92,6 +89,7 @@ lazy val webknossosDatastore = (project in file("webknossos-datastore")) } ((libs +++ subs +++ targets) ** "*.jar").classpath }, + routesImport += "com.scalableminds.util.objectid.ObjectId", copyMessagesFilesSetting ) @@ -102,11 +100,12 @@ lazy val webknossosTracingstore = (project in file("webknossos-tracingstore")) .settings( name := "webknossos-tracingstore", commonSettings, + routesImport += "com.scalableminds.util.objectid.ObjectId", generateReverseRouter := false, BuildInfoSettings.webknossosTracingstoreBuildInfoSettings, libraryDependencies ++= Dependencies.webknossosTracingstoreDependencies, dependencyOverrides ++= Dependencies.dependencyOverrides, - copyMessagesFilesSetting + copyMessagesFilesSetting, ) lazy val webknossos = (project in file(".")) @@ -116,6 +115,7 @@ lazy val webknossos = (project in file(".")) .settings( name := "webknossos", commonSettings, + routesImport += "com.scalableminds.util.objectid.ObjectId", generateReverseRouter := false, AssetCompilation.settings, BuildInfoSettings.webknossosBuildInfoSettings, diff --git a/conf/webknossos.latest.routes b/conf/webknossos.latest.routes index b5cdecaa007..628d92ec71f 100644 --- a/conf/webknossos.latest.routes +++ b/conf/webknossos.latest.routes @@ -28,7 +28,7 @@ GET /auth/token DELETE /auth/token controllers.AuthenticationController.deleteToken() GET /auth/switch controllers.AuthenticationController.switchMultiUser(to: String) POST /auth/switchOrganization/:organizationId controllers.AuthenticationController.switchOrganization(organizationId: String) -GET /auth/accessibleBySwitching controllers.AuthenticationController.accessibleBySwitching(datasetId: Option[ObjectId], annotationId: Option[String], workflowHash: Option[String]) +GET /auth/accessibleBySwitching controllers.AuthenticationController.accessibleBySwitching(datasetId: Option[ObjectId], annotationId: Option[ObjectId], workflowHash: Option[String]) POST /auth/sendInvites controllers.AuthenticationController.sendInvites() POST /auth/startResetPassword controllers.AuthenticationController.handleStartResetPassword() POST /auth/changePassword controllers.AuthenticationController.changePassword() diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/annotation/AnnotationSource.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/annotation/AnnotationSource.scala index cfd8e69180b..7d60eec44f3 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/annotation/AnnotationSource.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/models/annotation/AnnotationSource.scala @@ -1,8 +1,9 @@ package com.scalableminds.webknossos.datastore.models.annotation +import com.scalableminds.util.objectid.ObjectId import play.api.libs.json.{Json, OFormat} -case class AnnotationSource(id: String, +case class AnnotationSource(id: ObjectId, annotationLayers: List[AnnotationLayer], datasetDirectoryName: String, organizationId: String, diff --git a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/AccessTokenService.scala b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/AccessTokenService.scala index 941df655b7d..6c209d2ceda 100644 --- a/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/AccessTokenService.scala +++ b/webknossos-datastore/app/com/scalableminds/webknossos/datastore/services/AccessTokenService.scala @@ -4,6 +4,7 @@ import com.google.inject.Inject import com.scalableminds.util.accesscontext.TokenContext import com.scalableminds.util.cache.AlfuCache import com.scalableminds.util.enumeration.ExtendedEnumeration +import com.scalableminds.util.objectid.ObjectId import com.scalableminds.util.tools.Fox import com.scalableminds.webknossos.datastore.models.datasource.DataSourceId import play.api.libs.json.{Json, OFormat} @@ -47,11 +48,11 @@ object UserAccessRequest { def writeTracing(tracingId: String): UserAccessRequest = UserAccessRequest(DataSourceId(tracingId, ""), AccessResourceType.tracing, AccessMode.write) - def readAnnotation(annotationId: String): UserAccessRequest = - UserAccessRequest(DataSourceId(annotationId, ""), AccessResourceType.annotation, AccessMode.read) + def readAnnotation(annotationId: ObjectId): UserAccessRequest = + UserAccessRequest(DataSourceId(annotationId.toString, ""), AccessResourceType.annotation, AccessMode.read) - def writeAnnotation(annotationId: String): UserAccessRequest = - UserAccessRequest(DataSourceId(annotationId, ""), AccessResourceType.annotation, AccessMode.write) + def writeAnnotation(annotationId: ObjectId): UserAccessRequest = + UserAccessRequest(DataSourceId(annotationId.toString, ""), AccessResourceType.annotation, AccessMode.write) def downloadJobExport(jobId: String): UserAccessRequest = UserAccessRequest(DataSourceId(jobId, ""), AccessResourceType.jobExport, AccessMode.read) diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/TSRemoteDatastoreClient.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/TSRemoteDatastoreClient.scala index 5a5b2f064f4..20eee18c328 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/TSRemoteDatastoreClient.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/TSRemoteDatastoreClient.scala @@ -4,6 +4,7 @@ import com.google.inject.Inject import com.scalableminds.util.accesscontext.TokenContext import com.scalableminds.util.cache.AlfuCache import com.scalableminds.util.geometry.Vec3Int +import com.scalableminds.util.objectid.ObjectId import com.scalableminds.util.tools.Fox import com.scalableminds.webknossos.datastore.AgglomerateGraph.AgglomerateGraph import com.scalableminds.webknossos.datastore.ListOfLong.ListOfLong @@ -38,7 +39,7 @@ class TSRemoteDatastoreClient @Inject()( with MissingBucketHeaders { private lazy val dataStoreUriCache: AlfuCache[(String, String), String] = AlfuCache() - private lazy val voxelSizeCache: AlfuCache[String, VoxelSize] = AlfuCache(timeToLive = 10 minutes) + private lazy val voxelSizeCache: AlfuCache[ObjectId, VoxelSize] = AlfuCache(timeToLive = 10 minutes) private lazy val largestAgglomerateIdCache: AlfuCache[(RemoteFallbackLayer, String, Option[String]), Long] = AlfuCache(timeToLive = 10 minutes) @@ -160,10 +161,10 @@ class TSRemoteDatastoreClient @Inject()( .postJsonWithBytesResponse(fullMeshRequest) } yield result - def voxelSizeForAnnotationWithCache(annotationId: String)(implicit tc: TokenContext): Fox[VoxelSize] = + def voxelSizeForAnnotationWithCache(annotationId: ObjectId)(implicit tc: TokenContext): Fox[VoxelSize] = voxelSizeCache.getOrLoad(annotationId, aId => voxelSizeForAnnotation(aId)) - private def voxelSizeForAnnotation(annotationId: String)(implicit tc: TokenContext): Fox[VoxelSize] = + private def voxelSizeForAnnotation(annotationId: ObjectId)(implicit tc: TokenContext): Fox[VoxelSize] = for { dataSourceId <- remoteWebknossosClient.getDataSourceIdForAnnotation(annotationId) dataStoreUri <- dataStoreUriWithCache(dataSourceId.organizationId, dataSourceId.directoryName) diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/TSRemoteWebknossosClient.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/TSRemoteWebknossosClient.scala index eb845c08128..109926be669 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/TSRemoteWebknossosClient.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/TSRemoteWebknossosClient.scala @@ -3,6 +3,7 @@ package com.scalableminds.webknossos.tracingstore import com.google.inject.Inject import com.scalableminds.util.accesscontext.TokenContext import com.scalableminds.util.cache.AlfuCache +import com.scalableminds.util.objectid.ObjectId import com.scalableminds.util.time.Instant import com.scalableminds.util.tools.Fox import com.scalableminds.webknossos.datastore.Annotation.AnnotationProto @@ -27,7 +28,7 @@ import play.api.libs.ws.WSResponse import scala.concurrent.ExecutionContext import scala.concurrent.duration.DurationInt -case class AnnotationUpdatesReport(annotationId: String, +case class AnnotationUpdatesReport(annotationId: ObjectId, timestamps: List[Instant], statistics: Option[JsObject], significantChangesCount: Int, @@ -49,8 +50,8 @@ class TSRemoteWebknossosClient @Inject()( private val webknossosUri: String = config.Tracingstore.WebKnossos.uri - private lazy val dataSourceIdByAnnotationIdCache: AlfuCache[String, DataSourceId] = AlfuCache() - private lazy val annotationIdByTracingIdCache: AlfuCache[String, String] = + private lazy val dataSourceIdByAnnotationIdCache: AlfuCache[ObjectId, DataSourceId] = AlfuCache() + private lazy val annotationIdByTracingIdCache: AlfuCache[String, ObjectId] = AlfuCache(maxCapacity = 10000, timeToLive = 5 minutes) def reportAnnotationUpdates(tracingUpdatesReport: AnnotationUpdatesReport): Fox[WSResponse] = @@ -59,9 +60,9 @@ class TSRemoteWebknossosClient @Inject()( .silent .postJson(Json.toJson(tracingUpdatesReport)) - def getDataSourceForAnnotation(annotationId: String)(implicit tc: TokenContext): Fox[DataSourceLike] = + def getDataSourceForAnnotation(annotationId: ObjectId)(implicit tc: TokenContext): Fox[DataSourceLike] = rpc(s"$webknossosUri/api/tracingstores/$tracingStoreName/dataSource") - .addQueryString("annotationId" -> annotationId) + .addQueryString("annotationId" -> annotationId.toString) .addQueryString("key" -> tracingStoreKey) .withTokenFromContext .silent @@ -74,18 +75,18 @@ class TSRemoteWebknossosClient @Inject()( .silent .getWithJsonResponse[String] - def getDataSourceIdForAnnotation(annotationId: String)(implicit ec: ExecutionContext): Fox[DataSourceId] = + def getDataSourceIdForAnnotation(annotationId: ObjectId)(implicit ec: ExecutionContext): Fox[DataSourceId] = dataSourceIdByAnnotationIdCache.getOrLoad( annotationId, aId => rpc(s"$webknossosUri/api/tracingstores/$tracingStoreName/dataSourceId") - .addQueryString("annotationId" -> aId) + .addQueryString("annotationId" -> aId.toString) .addQueryString("key" -> tracingStoreKey) .silent .getWithJsonResponse[DataSourceId] ) - def getAnnotationIdForTracing(tracingId: String)(implicit ec: ExecutionContext): Fox[String] = + def getAnnotationIdForTracing(tracingId: String)(implicit ec: ExecutionContext): Fox[ObjectId] = annotationIdByTracingIdCache.getOrLoad( tracingId, tracingId => @@ -93,21 +94,21 @@ class TSRemoteWebknossosClient @Inject()( .addQueryString("tracingId" -> tracingId) .addQueryString("key" -> tracingStoreKey) .silent - .getWithJsonResponse[String] + .getWithJsonResponse[ObjectId] ) ?~> "annotation.idForTracing.failed" - def updateAnnotation(annotationId: String, annotationProto: AnnotationProto): Fox[Unit] = + def updateAnnotation(annotationId: ObjectId, annotationProto: AnnotationProto): Fox[Unit] = rpc(s"$webknossosUri/api/tracingstores/$tracingStoreName/updateAnnotation") - .addQueryString("annotationId" -> annotationId) + .addQueryString("annotationId" -> annotationId.toString) .addQueryString("key" -> tracingStoreKey) .silent .postProto(annotationProto) - def createTracingFor(annotationId: String, + def createTracingFor(annotationId: ObjectId, layerParameters: AnnotationLayerParameters, previousVersion: Long): Fox[Either[SkeletonTracingWithUpdatedTreeIds, VolumeTracing]] = { val req = rpc(s"$webknossosUri/api/tracingstores/$tracingStoreName/createTracing") - .addQueryString("annotationId" -> annotationId) + .addQueryString("annotationId" -> annotationId.toString) .addQueryString("previousVersion" -> previousVersion.toString) // used for fetching old precedence layers .addQueryString("key" -> tracingStoreKey) layerParameters.typ match { diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/annotation/AnnotationReversion.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/annotation/AnnotationReversion.scala index 09b740318f2..457681d69df 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/annotation/AnnotationReversion.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/annotation/AnnotationReversion.scala @@ -1,6 +1,7 @@ package com.scalableminds.webknossos.tracingstore.annotation import com.scalableminds.util.accesscontext.TokenContext +import com.scalableminds.util.objectid.ObjectId import com.scalableminds.util.tools.{Fox, FoxImplicits} import com.scalableminds.webknossos.datastore.EditableMappingInfo.EditableMappingInfo import com.scalableminds.webknossos.datastore.VolumeTracing.VolumeTracing @@ -20,14 +21,14 @@ trait AnnotationReversion extends FoxImplicits { version: Option[Long]): Fox[VersionedKeyValuePair[EditableMappingInfo]] protected def editableMappingUpdaterFor( - annotationId: String, + annotationId: ObjectId, tracingId: String, volumeTracing: VolumeTracing, editableMappingInfo: EditableMappingInfo, currentMaterializedVersion: Long, targetVersion: Long)(implicit tc: TokenContext, ec: ExecutionContext): Fox[EditableMappingUpdater] - def revertDistributedElements(annotationId: String, + def revertDistributedElements(annotationId: ObjectId, currentAnnotationWithTracings: AnnotationWithTracings, sourceAnnotationWithTracings: AnnotationWithTracings, sourceVersion: Long, @@ -60,7 +61,7 @@ trait AnnotationReversion extends FoxImplicits { } yield () private def revertEditableMappingFields( - annotationId: String, + annotationId: ObjectId, currentAnnotationWithTracings: AnnotationWithTracings, tracingBeforeRevert: VolumeTracing, sourceVersion: Long, @@ -75,7 +76,7 @@ trait AnnotationReversion extends FoxImplicits { } yield () // If source annotation doesn’t have this editable mapping, use the last existing one as a “before point” for the reversion - private def editableMappingReversionUpdater(annotationId: String, + private def editableMappingReversionUpdater(annotationId: ObjectId, tracingId: String, tracingBeforeRevert: VolumeTracing, targetVersion: Long)(implicit ec: ExecutionContext, tc: TokenContext) = diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/annotation/AnnotationTransactionService.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/annotation/AnnotationTransactionService.scala index c1bc0f06b14..673bb83214c 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/annotation/AnnotationTransactionService.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/annotation/AnnotationTransactionService.scala @@ -1,6 +1,7 @@ package com.scalableminds.webknossos.tracingstore.annotation import com.scalableminds.util.accesscontext.TokenContext +import com.scalableminds.util.objectid.ObjectId import com.scalableminds.util.time.Instant import com.scalableminds.util.tools.{Fox, JsonHelper} import com.scalableminds.webknossos.tracingstore.tracings.volume.{ @@ -34,19 +35,22 @@ class AnnotationTransactionService @Inject()(handledGroupIdStore: TracingStoreRe private val transactionGroupExpiry: FiniteDuration = 24 hours private val handledGroupCacheExpiry: FiniteDuration = 24 hours - private def transactionGroupKey(annotationId: String, + private def transactionGroupKey(annotationId: ObjectId, transactionId: String, transactionGroupIndex: Int, version: Long) = s"transactionGroup___${annotationId}___${transactionId}___${transactionGroupIndex}___$version" - private def handledGroupKey(annotationId: String, transactionId: String, version: Long, transactionGroupIndex: Int) = + private def handledGroupKey(annotationId: ObjectId, + transactionId: String, + version: Long, + transactionGroupIndex: Int) = s"handledGroup___${annotationId}___${transactionId}___${version}___$transactionGroupIndex" - private def patternFor(annotationId: String, transactionId: String) = + private def patternFor(annotationId: ObjectId, transactionId: String) = s"transactionGroup___${annotationId}___${transactionId}___*" - private def saveUncommitted(annotationId: String, + private def saveUncommitted(annotationId: ObjectId, transactionId: String, transactionGroupIndex: Int, version: Long, @@ -67,7 +71,7 @@ class AnnotationTransactionService @Inject()(handledGroupIdStore: TracingStoreRe } yield () private def handleUpdateGroupOfTransaction( - annotationId: String, + annotationId: ObjectId, previousVersionFox: Fox[Long], updateGroup: UpdateActionGroup)(implicit ec: ExecutionContext, tc: TokenContext): Fox[Long] = for { @@ -97,8 +101,8 @@ class AnnotationTransactionService @Inject()(handledGroupIdStore: TracingStoreRe // For an update group (that is the last of a transaction), fetch all previous uncommitted for the same transaction // and commit them all. - private def commitWithPending(annotationId: String, updateGroup: UpdateActionGroup)(implicit ec: ExecutionContext, - tc: TokenContext): Fox[Long] = + private def commitWithPending(annotationId: ObjectId, updateGroup: UpdateActionGroup)(implicit ec: ExecutionContext, + tc: TokenContext): Fox[Long] = for { previousActionGroupsToCommit <- getAllUncommittedFor(annotationId, updateGroup.transactionId) _ <- Fox.fromBool(previousActionGroupsToCommit @@ -108,17 +112,17 @@ class AnnotationTransactionService @Inject()(handledGroupIdStore: TracingStoreRe _ <- removeAllUncommittedFor(annotationId, updateGroup.transactionId) } yield commitResult - private def removeAllUncommittedFor(tracingId: String, transactionId: String): Fox[Unit] = - uncommittedUpdatesStore.removeAllConditional(patternFor(tracingId, transactionId)) + private def removeAllUncommittedFor(annotationId: ObjectId, transactionId: String): Fox[Unit] = + uncommittedUpdatesStore.removeAllConditional(patternFor(annotationId, transactionId)) - private def getAllUncommittedFor(annotationId: String, transactionId: String): Fox[List[UpdateActionGroup]] = + private def getAllUncommittedFor(annotationId: ObjectId, transactionId: String): Fox[List[UpdateActionGroup]] = for { raw: Seq[String] <- uncommittedUpdatesStore.findAllConditional(patternFor(annotationId, transactionId)) parsed: Seq[UpdateActionGroup] = raw.flatMap(itemAsString => JsonHelper.parseAs[UpdateActionGroup](itemAsString).toOption) } yield parsed.toList.sortBy(_.transactionGroupIndex) - private def saveToHandledGroupIdStore(annotationId: String, + private def saveToHandledGroupIdStore(annotationId: ObjectId, transactionId: String, version: Long, transactionGroupIndex: Int): Fox[Unit] = { @@ -126,7 +130,7 @@ class AnnotationTransactionService @Inject()(handledGroupIdStore: TracingStoreRe handledGroupIdStore.insert(key, "()", Some(handledGroupCacheExpiry)) } - private def handledGroupIdStoreContains(annotationId: String, + private def handledGroupIdStoreContains(annotationId: ObjectId, transactionId: String, version: Long, transactionGroupIndex: Int): Fox[Boolean] = @@ -150,7 +154,7 @@ class AnnotationTransactionService @Inject()(handledGroupIdStore: TracingStoreRe ) } - def handleSingleUpdateAction(annotationId: String, currentVersion: Long, updateAction: UpdateAction)( + def handleSingleUpdateAction(annotationId: ObjectId, currentVersion: Long, updateAction: UpdateAction)( implicit ec: ExecutionContext, tc: TokenContext): Fox[Long] = { val wrapped = List( @@ -168,8 +172,8 @@ class AnnotationTransactionService @Inject()(handledGroupIdStore: TracingStoreRe handleUpdateGroups(annotationId, wrapped) } - def handleUpdateGroups(annotationId: String, updateGroups: List[UpdateActionGroup])(implicit ec: ExecutionContext, - tc: TokenContext): Fox[Long] = + def handleUpdateGroups(annotationId: ObjectId, updateGroups: List[UpdateActionGroup])(implicit ec: ExecutionContext, + tc: TokenContext): Fox[Long] = if (updateGroups.forall(_.transactionGroupCount == 1)) { commitUpdates(annotationId, updateGroups) } else { @@ -180,8 +184,9 @@ class AnnotationTransactionService @Inject()(handledGroupIdStore: TracingStoreRe } // Perform version check and commit the passed updates - private def commitUpdates(annotationId: String, updateGroups: List[UpdateActionGroup])(implicit ec: ExecutionContext, - tc: TokenContext): Fox[Long] = + private def commitUpdates(annotationId: ObjectId, updateGroups: List[UpdateActionGroup])( + implicit ec: ExecutionContext, + tc: TokenContext): Fox[Long] = for { _ <- reportUpdates(annotationId, updateGroups) currentCommittedVersion: Fox[Long] = annotationService.currentMaterializableVersion(annotationId) @@ -201,7 +206,7 @@ class AnnotationTransactionService @Inject()(handledGroupIdStore: TracingStoreRe _ <- applyImmediatelyIfNeeded(annotationId, updateGroups.flatMap(_.actions), newVersion) } yield newVersion - private def applyImmediatelyIfNeeded(annotationId: String, updates: List[UpdateAction], newVersion: Long)( + private def applyImmediatelyIfNeeded(annotationId: ObjectId, updates: List[UpdateAction], newVersion: Long)( implicit ec: ExecutionContext, tc: TokenContext): Fox[Unit] = if (containsApplyImmediatelyUpdateActions(updates)) { @@ -213,12 +218,12 @@ class AnnotationTransactionService @Inject()(handledGroupIdStore: TracingStoreRe case _ => false } - private def handleUpdateGroup(annotationId: String, updateActionGroup: UpdateActionGroup)( + private def handleUpdateGroup(annotationId: ObjectId, updateActionGroup: UpdateActionGroup)( implicit ec: ExecutionContext, tc: TokenContext): Fox[Unit] = for { updateActionsJson <- Fox.successful(Json.toJson(preprocessActionsForStorage(updateActionGroup))) - _ <- tracingDataStore.annotationUpdates.put(annotationId, updateActionGroup.version, updateActionsJson) + _ <- tracingDataStore.annotationUpdates.put(annotationId.toString, updateActionGroup.version, updateActionsJson) bucketMutatingActions = findBucketMutatingActions(updateActionGroup) actionsGrouped: Map[String, List[BucketMutatingVolumeUpdateAction]] = bucketMutatingActions.groupBy( _.actionTracingId) @@ -258,7 +263,7 @@ class AnnotationTransactionService @Inject()(handledGroupIdStore: TracingStoreRe * ignore it silently. This is in case the frontend sends a retry if it believes a save to be unsuccessful * despite the backend receiving it just fine. */ - private def failUnlessAlreadyHandled(updateGroup: UpdateActionGroup, annotationId: String, previousVersion: Long)( + private def failUnlessAlreadyHandled(updateGroup: UpdateActionGroup, annotationId: ObjectId, previousVersion: Long)( implicit ec: ExecutionContext): Fox[Long] = { val errorMessage = s"Incorrect version. Expected: ${previousVersion + 1}; Got: ${updateGroup.version}" for { @@ -270,7 +275,7 @@ class AnnotationTransactionService @Inject()(handledGroupIdStore: TracingStoreRe } yield updateGroup.version } - private def reportUpdates(annotationId: String, updateGroups: List[UpdateActionGroup])( + private def reportUpdates(annotationId: ObjectId, updateGroups: List[UpdateActionGroup])( implicit tc: TokenContext): Fox[Unit] = for { _ <- remoteWebknossosClient.reportAnnotationUpdates( diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/annotation/TSAnnotationService.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/annotation/TSAnnotationService.scala index c284f3ad9df..ea974339f4a 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/annotation/TSAnnotationService.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/annotation/TSAnnotationService.scala @@ -4,6 +4,7 @@ import collections.SequenceUtils import com.scalableminds.util.accesscontext.TokenContext import com.scalableminds.util.cache.AlfuCache import com.scalableminds.util.geometry.{BoundingBox, Vec3Double, Vec3Int} +import com.scalableminds.util.objectid.ObjectId import com.scalableminds.util.time.Instant import com.scalableminds.util.tools.{Fox, FoxImplicits} import com.scalableminds.webknossos.datastore.Annotation.{ @@ -57,13 +58,13 @@ class TSAnnotationService @Inject()(val remoteWebknossosClient: TSRemoteWebknoss // two-level caching: outer key: annotation id; inner key version // This way we cache at most two versions of the same annotation, and at most 1000 different annotations private lazy val materializedAnnotationWithTracingCache = - AlfuCache[String, AlfuCache[Long, AnnotationWithTracings]](maxCapacity = 1000) + AlfuCache[ObjectId, AlfuCache[Long, AnnotationWithTracings]](maxCapacity = 1000) private def newInnerCache(implicit ec: ExecutionContext): Fox[AlfuCache[Long, AnnotationWithTracings]] = Fox.successful(AlfuCache[Long, AnnotationWithTracings](maxCapacity = 2)) - def get(annotationId: String, version: Option[Long])(implicit ec: ExecutionContext, - tc: TokenContext): Fox[AnnotationProto] = + def get(annotationId: ObjectId, version: Option[Long])(implicit ec: ExecutionContext, + tc: TokenContext): Fox[AnnotationProto] = for { isTemporaryAnnotation <- temporaryTracingService.isTemporaryAnnotation(annotationId) annotation <- if (isTemporaryAnnotation) temporaryTracingService.getAnnotation(annotationId) @@ -73,13 +74,13 @@ class TSAnnotationService @Inject()(val remoteWebknossosClient: TSRemoteWebknoss } yield withTracings.annotation } yield annotation - def getMultiple(annotationIds: Seq[String])(implicit ec: ExecutionContext, - tc: TokenContext): Fox[Seq[AnnotationProto]] = + def getMultiple(annotationIds: Seq[ObjectId])(implicit ec: ExecutionContext, + tc: TokenContext): Fox[Seq[AnnotationProto]] = Fox.serialCombined(annotationIds) { annotationId => get(annotationId, None) } - private def getWithTracings(annotationId: String, version: Option[Long])( + private def getWithTracings(annotationId: ObjectId, version: Option[Long])( implicit ec: ExecutionContext, tc: TokenContext): Fox[AnnotationWithTracings] = for { @@ -96,7 +97,7 @@ class TSAnnotationService @Inject()(val remoteWebknossosClient: TSRemoteWebknoss ) } yield updatedAnnotation - private def getWithTracingsVersioned(annotationId: String, targetVersion: Long, reportChangesToWk: Boolean)( + private def getWithTracingsVersioned(annotationId: ObjectId, targetVersion: Long, reportChangesToWk: Boolean)( implicit ec: ExecutionContext, tc: TokenContext): Fox[AnnotationWithTracings] = for { @@ -111,11 +112,13 @@ class TSAnnotationService @Inject()(val remoteWebknossosClient: TSRemoteWebknoss updated <- applyPendingUpdates(annotationWithTracingsAndMappings, annotationId, targetVersion, reportChangesToWk) ?~> "applyUpdates.failed" } yield updated - def currentMaterializableVersion(annotationId: String): Fox[Long] = - tracingDataStore.annotationUpdates.getVersion(annotationId, mayBeEmpty = Some(true), emptyFallback = Some(0L)) + def currentMaterializableVersion(annotationId: ObjectId): Fox[Long] = + tracingDataStore.annotationUpdates.getVersion(annotationId.toString, + mayBeEmpty = Some(true), + emptyFallback = Some(0L)) - def currentMaterializedVersion(annotationId: String): Fox[Long] = - tracingDataStore.annotations.getVersion(annotationId, mayBeEmpty = Some(true), emptyFallback = Some(0L)) + def currentMaterializedVersion(annotationId: ObjectId): Fox[Long] = + tracingDataStore.annotations.getVersion(annotationId.toString, mayBeEmpty = Some(true), emptyFallback = Some(0L)) private def newestMatchingMaterializedSkeletonVersion(tracingId: String, targetVersion: Long): Fox[Long] = tracingDataStore.skeletons.getVersion(tracingId, @@ -129,17 +132,17 @@ class TSAnnotationService @Inject()(val remoteWebknossosClient: TSRemoteWebknoss mayBeEmpty = Some(true), emptyFallback = Some(0L)) - private def getNewestMatchingMaterializedAnnotation(annotationId: String, + private def getNewestMatchingMaterializedAnnotation(annotationId: ObjectId, version: Option[Long]): Fox[AnnotationProto] = for { keyValuePair <- tracingDataStore.annotations.get[AnnotationProto]( - annotationId, + annotationId.toString, mayBeEmpty = Some(true), version = version)(fromProtoBytes[AnnotationProto]) ?~> "getAnnotation.failed" } yield keyValuePair.value private def applyUpdate( - annotationId: String, + annotationId: ObjectId, annotationWithTracings: AnnotationWithTracings, updateAction: UpdateAction, targetVersion: Long // Note: this is not the target version of this one update, but of all pending @@ -175,7 +178,7 @@ class TSAnnotationService @Inject()(val remoteWebknossosClient: TSRemoteWebknoss case _ => Fox.failure(s"Received unsupported AnnotationUpdateAction action ${Json.toJson(updateAction)}") } - private def addLayer(annotationId: String, + private def addLayer(annotationId: ObjectId, annotationWithTracings: AnnotationWithTracings, action: AddLayerAnnotationAction, targetVersion: Long)(implicit ec: ExecutionContext): Fox[AnnotationWithTracings] = @@ -194,7 +197,7 @@ class TSAnnotationService @Inject()(val remoteWebknossosClient: TSRemoteWebknoss } yield updated private def revertToVersion( - annotationId: String, + annotationId: ObjectId, annotationWithTracings: AnnotationWithTracings, revertAction: RevertToVersionAnnotationAction, newVersion: Long)(implicit ec: ExecutionContext, tc: TokenContext): Fox[AnnotationWithTracings] = @@ -213,7 +216,7 @@ class TSAnnotationService @Inject()(val remoteWebknossosClient: TSRemoteWebknoss s"Reverting annotation $annotationId from v${annotationWithTracings.version} to v${revertAction.sourceVersion}") } yield sourceAnnotation.markAllTreeBodiesAsChanged - private def resetToBase(annotationId: String, annotationWithTracings: AnnotationWithTracings, newVersion: Long)( + private def resetToBase(annotationId: ObjectId, annotationWithTracings: AnnotationWithTracings, newVersion: Long)( implicit ec: ExecutionContext, tc: TokenContext): Fox[AnnotationWithTracings] = { // Note: works only if reset actions are in separate update groups @@ -226,16 +229,16 @@ class TSAnnotationService @Inject()(val remoteWebknossosClient: TSRemoteWebknoss } yield sourceAnnotation.markAllTreeBodiesAsChanged } - def saveAnnotationProto(annotationId: String, + def saveAnnotationProto(annotationId: ObjectId, version: Long, annotationProto: AnnotationProto, toTemporaryStore: Boolean = false): Fox[Unit] = if (toTemporaryStore) temporaryTracingService.saveAnnotationProto(annotationId, annotationProto) else - tracingDataStore.annotations.put(annotationId, version, annotationProto) + tracingDataStore.annotations.put(annotationId.toString, version, annotationProto) - def updateActionLog(annotationId: String, newestVersion: Long, oldestVersion: Long)( + def updateActionLog(annotationId: ObjectId, newestVersion: Long, oldestVersion: Long)( implicit ec: ExecutionContext): Fox[JsValue] = { def versionedTupleToJson(tuple: (Long, List[UpdateAction])): JsObject = Json.obj( @@ -249,14 +252,14 @@ class TSAnnotationService @Inject()(val remoteWebknossosClient: TSRemoteWebknoss val batchFrom = batchRange._1 val batchTo = batchRange._2 tracingDataStore.annotationUpdates.getMultipleVersionsAsVersionValueTuple( - annotationId, + annotationId.toString, Some(batchTo), Some(batchFrom))(fromJsonBytes[List[UpdateAction]]) } } yield Json.toJson(updateActionBatches.flatten.map(versionedTupleToJson)) } - def findEditableMappingInfo(annotationId: String, tracingId: String, version: Option[Long] = None)( + def findEditableMappingInfo(annotationId: ObjectId, tracingId: String, version: Option[Long] = None)( implicit ec: ExecutionContext, tc: TokenContext): Fox[EditableMappingInfo] = for { @@ -265,7 +268,7 @@ class TSAnnotationService @Inject()(val remoteWebknossosClient: TSRemoteWebknoss } yield tracing private def addEditableMapping( - annotationId: String, + annotationId: ObjectId, annotationWithTracings: AnnotationWithTracings, action: UpdateMappingNameVolumeAction, targetVersion: Long)(implicit tc: TokenContext, ec: ExecutionContext): Fox[AnnotationWithTracings] = @@ -288,7 +291,7 @@ class TSAnnotationService @Inject()(val remoteWebknossosClient: TSRemoteWebknoss private def applyPendingUpdates( annotationWithTracingsAndMappings: AnnotationWithTracings, - annotationId: String, + annotationId: ObjectId, targetVersion: Long, reportChangesToWk: Boolean)(implicit ec: ExecutionContext, tc: TokenContext): Fox[AnnotationWithTracings] = for { @@ -301,7 +304,7 @@ class TSAnnotationService @Inject()(val remoteWebknossosClient: TSRemoteWebknoss } yield updated.withVersion(targetVersion) // set version again, because extraSkeleton update filtering may skip latest version - private def findPendingUpdates(annotationId: String, annotation: AnnotationWithTracings, desiredVersion: Long)( + private def findPendingUpdates(annotationId: ObjectId, annotation: AnnotationWithTracings, desiredVersion: Long)( implicit ec: ExecutionContext): Fox[List[(Long, List[UpdateAction])]] = for { extraSkeletonUpdates <- findExtraSkeletonUpdates(annotationId, annotation, desiredVersion) @@ -310,7 +313,7 @@ class TSAnnotationService @Inject()(val remoteWebknossosClient: TSRemoteWebknoss pendingAnnotationUpdates <- if (desiredVersion == existingVersion) Fox.successful(List.empty) else { tracingDataStore.annotationUpdates.getMultipleVersionsAsVersionValueTuple( - annotationId, + annotationId.toString, Some(desiredVersion), Some(existingVersion + 1))(fromJsonBytes[List[UpdateAction]]) } @@ -325,7 +328,7 @@ class TSAnnotationService @Inject()(val remoteWebknossosClient: TSRemoteWebknoss * we may fetch skeleton updates *older* than it, in order to fully construct the state of that version. * Only annotations from before that migration have this skeletonMayHavePendingUpdates=Some(true). */ - private def findExtraSkeletonUpdates(annotationId: String, annotation: AnnotationWithTracings, targetVersion: Long)( + private def findExtraSkeletonUpdates(annotationId: ObjectId, annotation: AnnotationWithTracings, targetVersion: Long)( implicit ec: ExecutionContext): Fox[List[(Long, List[UpdateAction])]] = if (annotation.annotation.skeletonMayHavePendingUpdates.getOrElse(false)) { annotation.getSkeletonId.map { skeletonId => @@ -333,7 +336,7 @@ class TSAnnotationService @Inject()(val remoteWebknossosClient: TSRemoteWebknoss materializedSkeletonVersion <- newestMatchingMaterializedSkeletonVersion(skeletonId, targetVersion) extraUpdates <- if (materializedSkeletonVersion < annotation.version) { tracingDataStore.annotationUpdates.getMultipleVersionsAsVersionValueTuple( - annotationId, + annotationId.toString, Some(annotation.version), Some(materializedSkeletonVersion + 1))(fromJsonBytes[List[UpdateAction]]) } else Fox.successful(List.empty) @@ -359,7 +362,7 @@ class TSAnnotationService @Inject()(val remoteWebknossosClient: TSRemoteWebknoss // Note that the EditableMappingUpdaters are passed only the “oldVersion” that is the materialized annotation version // not the actual materialized editableMapping version, but that should yield the same data when loading from fossil. private def findExtraEditableMappingUpdates( - annotationId: String, + annotationId: ObjectId, annotation: AnnotationWithTracings, targetVersion: Long)(implicit ec: ExecutionContext): Fox[List[(Long, List[UpdateAction])]] = if (annotation.annotation.editableMappingsMayHavePendingUpdates.getOrElse(false)) { @@ -370,7 +373,7 @@ class TSAnnotationService @Inject()(val remoteWebknossosClient: TSRemoteWebknoss targetVersion) extraUpdates <- if (materializedEditableMappingVersion < annotation.version) { tracingDataStore.annotationUpdates.getMultipleVersionsAsVersionValueTuple( - annotationId, + annotationId.toString, Some(annotation.version), Some(materializedEditableMappingVersion + 1))(fromJsonBytes[List[UpdateAction]]) } else Fox.successful(List.empty) @@ -416,7 +419,7 @@ class TSAnnotationService @Inject()(val remoteWebknossosClient: TSRemoteWebknoss } private def findEditableMappingsForAnnotation( - annotationId: String, + annotationId: ObjectId, annotationWithTracings: AnnotationWithTracings, currentMaterializedVersion: Long, targetVersion: Long)(implicit ec: ExecutionContext, tc: TokenContext) = { @@ -441,7 +444,7 @@ class TSAnnotationService @Inject()(val remoteWebknossosClient: TSRemoteWebknoss version: Option[Long]): Fox[VersionedKeyValuePair[EditableMappingInfo]] = tracingDataStore.editableMappingsInfo.get(volumeTracingId, version = version)(fromProtoBytes[EditableMappingInfo]) - private def editableMappingUpdaterFor(annotationId: String, + private def editableMappingUpdaterFor(annotationId: ObjectId, tracingId: String, remoteFallbackLayer: RemoteFallbackLayer, editableMappingInfo: EditableMappingInfo, @@ -461,7 +464,7 @@ class TSAnnotationService @Inject()(val remoteWebknossosClient: TSRemoteWebknoss ) protected def editableMappingUpdaterFor( - annotationId: String, + annotationId: ObjectId, tracingId: String, volumeTracing: VolumeTracing, editableMappingInfo: EditableMappingInfo, @@ -479,7 +482,7 @@ class TSAnnotationService @Inject()(val remoteWebknossosClient: TSRemoteWebknoss private def applyUpdatesGrouped( annotation: AnnotationWithTracings, - annotationId: String, + annotationId: ObjectId, updateGroups: List[(Long, List[UpdateAction])], reportChangesToWk: Boolean )(implicit ec: ExecutionContext, tc: TokenContext): Fox[AnnotationWithTracings] = { @@ -503,7 +506,7 @@ class TSAnnotationService @Inject()(val remoteWebknossosClient: TSRemoteWebknoss private def applyUpdates( annotationWithTracings: AnnotationWithTracings, - annotationId: String, + annotationId: ObjectId, updates: List[UpdateAction], targetVersion: Long, reportChangesToWk: Boolean)(implicit ec: ExecutionContext, tc: TokenContext): Fox[AnnotationWithTracings] = { @@ -590,10 +593,10 @@ class TSAnnotationService @Inject()(val remoteWebknossosClient: TSRemoteWebknoss flushOnlyTheseTreeIds = Some(updatedTreeIds)) } yield () - private def flushAnnotationInfo(annotationId: String, annotationWithTracings: AnnotationWithTracings) = + private def flushAnnotationInfo(annotationId: ObjectId, annotationWithTracings: AnnotationWithTracings) = saveAnnotationProto(annotationId, annotationWithTracings.version, annotationWithTracings.annotation) - private def determineTargetVersion(annotationId: String, + private def determineTargetVersion(annotationId: ObjectId, newestMaterializedAnnotation: AnnotationProto, requestedVersionOpt: Option[Long]): Fox[Long] = /* @@ -602,7 +605,7 @@ class TSAnnotationService @Inject()(val remoteWebknossosClient: TSRemoteWebknoss * hence the emptyFallbck newestMaterializedAnnotation.version) */ for { - newestUpdateVersion <- tracingDataStore.annotationUpdates.getVersion(annotationId, + newestUpdateVersion <- tracingDataStore.annotationUpdates.getVersion(annotationId.toString, mayBeEmpty = Some(true), emptyFallback = Some(newestMaterializedAnnotation.version)) @@ -614,7 +617,7 @@ class TSAnnotationService @Inject()(val remoteWebknossosClient: TSRemoteWebknoss } } yield targetVersion - def editableMappingLayer(annotationId: String, tracingId: String, tracing: VolumeTracing): EditableMappingLayer = + def editableMappingLayer(annotationId: ObjectId, tracingId: String, tracing: VolumeTracing): EditableMappingLayer = EditableMappingLayer( name = tracingId, tracing.boundingBox, @@ -627,7 +630,7 @@ class TSAnnotationService @Inject()(val remoteWebknossosClient: TSRemoteWebknoss editableMappingService = editableMappingService ) - def baseMappingName(annotationId: String, tracingId: String, tracing: VolumeTracing)( + def baseMappingName(annotationId: ObjectId, tracingId: String, tracing: VolumeTracing)( implicit ec: ExecutionContext, tc: TokenContext): Fox[Option[String]] = if (tracing.getHasEditableMapping) @@ -663,7 +666,7 @@ class TSAnnotationService @Inject()(val remoteWebknossosClient: TSRemoteWebknoss tracingDataStore.skeletons .get[SkeletonTracing](tracingId, version, mayBeEmpty = Some(true))(fromProtoBytes[SkeletonTracing]) - def findVolume(annotationId: String, tracingId: String, version: Option[Long] = None)( + def findVolume(annotationId: ObjectId, tracingId: String, version: Option[Long] = None)( implicit tc: TokenContext, ec: ExecutionContext): Fox[VolumeTracing] = for { @@ -678,7 +681,7 @@ class TSAnnotationService @Inject()(val remoteWebknossosClient: TSRemoteWebknoss } yield tracing def findSkeleton( - annotationId: String, + annotationId: ObjectId, tracingId: String, version: Option[Long] = None )(implicit tc: TokenContext, ec: ExecutionContext): Fox[SkeletonTracing] = @@ -726,8 +729,8 @@ class TSAnnotationService @Inject()(val remoteWebknossosClient: TSRemoteWebknoss } def duplicate( - annotationId: String, - newAnnotationId: String, + annotationId: ObjectId, + newAnnotationId: ObjectId, version: Option[Long], isFromTask: Boolean, datasetBoundingBox: Option[BoundingBox])(implicit ec: ExecutionContext, tc: TokenContext): Fox[AnnotationProto] = @@ -776,8 +779,8 @@ class TSAnnotationService @Inject()(val remoteWebknossosClient: TSRemoteWebknoss } yield duplicatedAnnotation - private def duplicateUpdates(annotationId: String, - newAnnotationId: String, + private def duplicateUpdates(annotationId: ObjectId, + newAnnotationId: ObjectId, v0TracingIds: Seq[String], newestVersion: Long)(implicit ec: ExecutionContext): Fox[Map[String, String]] = { val tracingIdMapMutable = scala.collection.mutable.Map[String, String]() @@ -791,7 +794,7 @@ class TSAnnotationService @Inject()(val remoteWebknossosClient: TSRemoteWebknoss for { updateLists: Seq[(Long, List[UpdateAction])] <- tracingDataStore.annotationUpdates .getMultipleVersionsAsVersionValueTuple( - annotationId, + annotationId.toString, oldestVersion = Some(batchRange._1), newestVersion = Some(batchRange._2))(fromJsonBytes[List[UpdateAction]]) _ <- Fox.serialCombined(updateLists.reverse) { // we reverse (order asc by version) so that addLayer comes before the layer’s updates @@ -818,7 +821,7 @@ class TSAnnotationService @Inject()(val remoteWebknossosClient: TSRemoteWebknoss case a: UpdateAction => Fox.successful(a) } - _ <- updatesPutBuffer.put(newAnnotationId, version, Json.toJson(updateListAdapted)) + _ <- updatesPutBuffer.put(newAnnotationId.toString, version, Json.toJson(updateListAdapted)) } yield () } } yield () @@ -827,8 +830,8 @@ class TSAnnotationService @Inject()(val remoteWebknossosClient: TSRemoteWebknoss .map(_ => tracingIdMapMutable.toMap) } - private def duplicateLayer(annotationId: String, - newAnnotationId: String, + private def duplicateLayer(annotationId: ObjectId, + newAnnotationId: ObjectId, layer: AnnotationLayerProto, tracingIdMap: Map[String, String], version: Long, @@ -866,10 +869,10 @@ class TSAnnotationService @Inject()(val remoteWebknossosClient: TSRemoteWebknoss } yield layer.copy(tracingId = newTracingId) def duplicateVolumeTracing( - sourceAnnotationId: String, + sourceAnnotationId: ObjectId, sourceTracingId: String, sourceVersion: Long, - newAnnotationId: String, + newAnnotationId: ObjectId, newTracingId: String, newVersion: Long, isFromTask: Boolean, @@ -904,7 +907,7 @@ class TSAnnotationService @Inject()(val remoteWebknossosClient: TSRemoteWebknoss duplicateEditableMapping(sourceAnnotationId, sourceTracingId, newTracingId, sourceVersion, newVersion)) } yield newTracingId - private def duplicateEditableMapping(sourceAnnotationId: String, + private def duplicateEditableMapping(sourceAnnotationId: ObjectId, sourceTracingId: String, newTracingId: String, sourceVersion: Long, @@ -920,7 +923,7 @@ class TSAnnotationService @Inject()(val remoteWebknossosClient: TSRemoteWebknoss } yield () def duplicateSkeletonTracing( - sourceAnnotationId: String, + sourceAnnotationId: ObjectId, sourceTracingId: String, sourceVersion: Long, newTracingId: String, diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/controllers/EditableMappingController.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/controllers/EditableMappingController.scala index cf8db771d07..4e83b333082 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/controllers/EditableMappingController.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/controllers/EditableMappingController.scala @@ -1,6 +1,7 @@ package com.scalableminds.webknossos.tracingstore.controllers import com.google.inject.Inject +import com.scalableminds.util.objectid.ObjectId import com.scalableminds.util.tools.Fox import com.scalableminds.webknossos.datastore.AgglomerateGraph.AgglomerateGraph import com.scalableminds.webknossos.datastore.ListOfLong.ListOfLong @@ -28,7 +29,7 @@ class EditableMappingController @Inject()( editableMappingService: EditableMappingService)(implicit ec: ExecutionContext, bodyParsers: PlayBodyParsers) extends Controller { - def editableMappingInfo(tracingId: String, annotationId: String, version: Option[Long]): Action[AnyContent] = + def editableMappingInfo(tracingId: String, annotationId: ObjectId, version: Option[Long]): Action[AnyContent] = Action.async { implicit request => log() { accessTokenService.validateAccessFromTokenContext(UserAccessRequest.readAnnotation(annotationId)) { @@ -64,7 +65,7 @@ class EditableMappingController @Inject()( } } - def agglomerateIdsForSegments(tracingId: String, annotationId: String, version: Option[Long]): Action[ListOfLong] = + def agglomerateIdsForSegments(tracingId: String, annotationId: ObjectId, version: Option[Long]): Action[ListOfLong] = Action.async(validateProto[ListOfLong]) { implicit request => log() { accessTokenService.validateAccessFromTokenContext(UserAccessRequest.readAnnotation(annotationId)) { diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/controllers/SkeletonTracingController.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/controllers/SkeletonTracingController.scala index 064a2369c80..e8c433a74a3 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/controllers/SkeletonTracingController.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/controllers/SkeletonTracingController.scala @@ -2,6 +2,7 @@ package com.scalableminds.webknossos.tracingstore.controllers import com.google.inject.Inject import com.scalableminds.util.geometry.{BoundingBox, Vec3Double, Vec3Int} +import com.scalableminds.util.objectid.ObjectId import com.scalableminds.util.tools.Fox import com.scalableminds.util.tools.JsonHelper.{boxFormat, optionFormat} import com.scalableminds.webknossos.datastore.SkeletonTracing.{ @@ -77,7 +78,7 @@ class SkeletonTracingController @Inject()(skeletonTracingService: SkeletonTracin } } - def get(tracingId: String, annotationId: String, version: Option[Long]): Action[AnyContent] = + def get(tracingId: String, annotationId: ObjectId, version: Option[Long]): Action[AnyContent] = Action.async { implicit request => log() { accessTokenService.validateAccessFromTokenContext(UserAccessRequest.readAnnotation(annotationId)) { diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/controllers/TSAnnotationController.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/controllers/TSAnnotationController.scala index 758b80ff427..0622a5bb909 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/controllers/TSAnnotationController.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/controllers/TSAnnotationController.scala @@ -3,22 +3,14 @@ package com.scalableminds.webknossos.tracingstore.controllers import collections.SequenceUtils import com.google.inject.Inject import com.scalableminds.util.geometry.BoundingBox +import com.scalableminds.util.objectid.ObjectId import com.scalableminds.util.tools.Fox -import com.scalableminds.webknossos.datastore.Annotation.{ - AnnotationLayerProto, - AnnotationLayerTypeProto, - AnnotationProto -} +import com.scalableminds.webknossos.datastore.Annotation.{AnnotationLayerProto, AnnotationLayerTypeProto, AnnotationProto} import com.scalableminds.webknossos.datastore.controllers.Controller import com.scalableminds.webknossos.datastore.models.annotation.AnnotationLayer import com.scalableminds.webknossos.datastore.services.UserAccessRequest import com.scalableminds.webknossos.tracingstore.TracingStoreAccessTokenService -import com.scalableminds.webknossos.tracingstore.annotation.{ - AnnotationTransactionService, - ResetToBaseAnnotationAction, - TSAnnotationService, - UpdateActionGroup -} +import com.scalableminds.webknossos.tracingstore.annotation.{AnnotationTransactionService, ResetToBaseAnnotationAction, TSAnnotationService, UpdateActionGroup} import com.scalableminds.webknossos.tracingstore.slacknotification.TSSlackNotificationService import com.scalableminds.webknossos.tracingstore.tracings._ import com.scalableminds.webknossos.tracingstore.tracings.editablemapping.EditableMappingMergeService @@ -42,7 +34,7 @@ class TSAnnotationController @Inject()( extends Controller with KeyValueStoreImplicits { - def save(annotationId: String, toTemporaryStore: Boolean = false): Action[AnnotationProto] = + def save(annotationId: ObjectId, toTemporaryStore: Boolean = false): Action[AnnotationProto] = Action.async(validateProto[AnnotationProto]) { implicit request => log() { accessTokenService.validateAccessFromTokenContext(UserAccessRequest.webknossos) { @@ -53,7 +45,7 @@ class TSAnnotationController @Inject()( } } - def update(annotationId: String): Action[List[UpdateActionGroup]] = + def update(annotationId: ObjectId): Action[List[UpdateActionGroup]] = Action.async(validateJson[List[UpdateActionGroup]]) { implicit request => log() { logTime(slackNotificationService.noticeSlowRequest) { @@ -66,7 +58,7 @@ class TSAnnotationController @Inject()( } } - def updateActionLog(annotationId: String, + def updateActionLog(annotationId: ObjectId, newestVersion: Option[Long] = None, oldestVersion: Option[Long] = None): Action[AnyContent] = Action.async { implicit request => log() { @@ -81,7 +73,7 @@ class TSAnnotationController @Inject()( } } - def newestVersion(annotationId: String): Action[AnyContent] = Action.async { implicit request => + def newestVersion(annotationId: ObjectId): Action[AnyContent] = Action.async { implicit request => log() { accessTokenService.validateAccessFromTokenContext(UserAccessRequest.readAnnotation(annotationId)) { for { @@ -91,7 +83,7 @@ class TSAnnotationController @Inject()( } } - def get(annotationId: String, version: Option[Long]): Action[AnyContent] = + def get(annotationId: ObjectId, version: Option[Long]): Action[AnyContent] = Action.async { implicit request => log() { logTime(slackNotificationService.noticeSlowRequest) { @@ -104,8 +96,8 @@ class TSAnnotationController @Inject()( } } - def duplicate(annotationId: String, - newAnnotationId: String, + def duplicate(annotationId: ObjectId, + newAnnotationId: ObjectId, version: Option[Long], isFromTask: Boolean, datasetBoundingBox: Option[String]): Action[AnyContent] = @@ -126,7 +118,7 @@ class TSAnnotationController @Inject()( } } - def resetToBase(annotationId: String): Action[AnyContent] = + def resetToBase(annotationId: ObjectId): Action[AnyContent] = Action.async { implicit request => log() { logTime(slackNotificationService.noticeSlowRequest) { @@ -142,8 +134,8 @@ class TSAnnotationController @Inject()( } } - def mergedFromIds(toTemporaryStore: Boolean, newAnnotationId: String): Action[List[String]] = - Action.async(validateJson[List[String]]) { implicit request => + def mergedFromIds(toTemporaryStore: Boolean, newAnnotationId: ObjectId): Action[List[ObjectId]] = + Action.async(validateJson[List[ObjectId]]) { implicit request => log() { accessTokenService.validateAccessFromTokenContext(UserAccessRequest.webknossos) { for { diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/controllers/VolumeTracingController.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/controllers/VolumeTracingController.scala index 84975f2eac6..95820f48516 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/controllers/VolumeTracingController.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/controllers/VolumeTracingController.scala @@ -3,6 +3,7 @@ package com.scalableminds.webknossos.tracingstore.controllers import collections.SequenceUtils import com.google.inject.Inject import com.scalableminds.util.geometry.{BoundingBox, Vec3Double, Vec3Int} +import com.scalableminds.util.objectid.ObjectId import com.scalableminds.util.tools.ExtendedTypes.ExtendedString import com.scalableminds.util.tools.Fox import com.scalableminds.util.tools.JsonHelper.optionFormat @@ -87,7 +88,7 @@ class VolumeTracingController @Inject()( } } - def get(tracingId: String, annotationId: String, version: Option[Long]): Action[AnyContent] = + def get(tracingId: String, annotationId: ObjectId, version: Option[Long]): Action[AnyContent] = Action.async { implicit request => log() { accessTokenService.validateAccessFromTokenContext(UserAccessRequest.readAnnotation(annotationId)) { @@ -111,7 +112,7 @@ class VolumeTracingController @Inject()( } } - def initialData(annotationId: String, + def initialData(annotationId: ObjectId, tracingId: String, minMag: Option[Int], maxMag: Option[Int]): Action[AnyContent] = @@ -154,7 +155,7 @@ class VolumeTracingController @Inject()( } } - def initialDataMultiple(annotationId: String, tracingId: String): Action[AnyContent] = + def initialDataMultiple(annotationId: ObjectId, tracingId: String): Action[AnyContent] = Action.async { implicit request => log() { logTime(slackNotificationService.noticeSlowRequest) { @@ -175,7 +176,7 @@ class VolumeTracingController @Inject()( } def allDataZip(tracingId: String, - annotationId: Option[String], + annotationId: Option[ObjectId], version: Option[Long], volumeDataZipFormat: String, voxelSizeFactor: Option[String], @@ -207,7 +208,7 @@ class VolumeTracingController @Inject()( } } - def data(tracingId: String, annotationId: String): Action[List[WebknossosDataRequest]] = + def data(tracingId: String, annotationId: ObjectId): Action[List[WebknossosDataRequest]] = Action.async(validateJson[List[WebknossosDataRequest]]) { implicit request => log() { accessTokenService.validateAccessFromTokenContext(UserAccessRequest.readAnnotation(annotationId)) { @@ -374,7 +375,7 @@ class VolumeTracingController @Inject()( // Used in task creation. History is dropped. Caller is responsible to create and save a matching AnnotationProto object def duplicate(tracingId: String, - newAnnotationId: String, + newAnnotationId: ObjectId, newTracingId: String, minMag: Option[Int], maxMag: Option[Int], diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/controllers/VolumeTracingZarrStreamingController.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/controllers/VolumeTracingZarrStreamingController.scala index 8330a386eb3..6a03c03d6f0 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/controllers/VolumeTracingZarrStreamingController.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/controllers/VolumeTracingZarrStreamingController.scala @@ -4,6 +4,7 @@ import com.google.inject.Inject import com.scalableminds.util.accesscontext.TokenContext import com.scalableminds.util.geometry.Vec3Int import com.scalableminds.util.mvc.ExtendedController +import com.scalableminds.util.objectid.ObjectId import com.scalableminds.util.tools.{Fox, FoxImplicits} import com.scalableminds.webknossos.datastore.VolumeTracing.VolumeTracing import com.scalableminds.webknossos.datastore.dataformats.MagLocator @@ -329,7 +330,7 @@ class VolumeTracingZarrStreamingController @Inject()( private def getFallbackLayerDataIfEmpty( tracing: VolumeTracing, - annotationId: String, + annotationId: ObjectId, data: Array[Byte], missingBucketIndices: List[Int], mag: Vec3Int, diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/RemoteFallbackLayer.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/RemoteFallbackLayer.scala index ddf984b3b4d..4250e156bfe 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/RemoteFallbackLayer.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/RemoteFallbackLayer.scala @@ -2,6 +2,7 @@ package com.scalableminds.webknossos.tracingstore.tracings import com.scalableminds.util.accesscontext.TokenContext import com.scalableminds.util.cache.AlfuCache +import com.scalableminds.util.objectid.ObjectId import com.scalableminds.util.tools.{Fox, FoxImplicits} import com.scalableminds.webknossos.datastore.VolumeTracing.VolumeTracing import com.scalableminds.webknossos.datastore.VolumeTracing.VolumeTracing.ElementClassProto @@ -33,7 +34,7 @@ trait FallbackDataHelper extends FoxImplicits { private lazy val fallbackBucketDataCache: AlfuCache[FallbackDataKey, (Array[Byte], List[Int])] = AlfuCache(maxCapacity = 3000) - def remoteFallbackLayerForVolumeTracing(tracing: VolumeTracing, annotationId: String)( + def remoteFallbackLayerForVolumeTracing(tracing: VolumeTracing, annotationId: ObjectId)( implicit ec: ExecutionContext): Fox[RemoteFallbackLayer] = for { layerName <- tracing.fallbackLayer.toFox ?~> "This feature is only defined on volume annotations with fallback segmentation layer." diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/TemporaryTracingService.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/TemporaryTracingService.scala index fbf64c0eda1..bd01b6cd7bd 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/TemporaryTracingService.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/TemporaryTracingService.scala @@ -1,5 +1,6 @@ package com.scalableminds.webknossos.tracingstore.tracings +import com.scalableminds.util.objectid.ObjectId import com.scalableminds.util.tools.{Fox, FoxImplicits} import com.scalableminds.webknossos.datastore.Annotation.AnnotationProto import com.scalableminds.webknossos.datastore.SkeletonTracing.SkeletonTracing @@ -37,10 +38,10 @@ class TemporaryTracingService @Inject()(skeletonStore: TemporaryTracingStore[Ske private def temporaryTracingIdKey(tracingId: String) = s"temporaryTracingId___$tracingId" - private def temporaryAnnotationIdKey(tracingId: String) = - s"temporaryTracingId___$tracingId" + private def temporaryAnnotationIdKey(annotationId: ObjectId) = + s"temporaryAnnotationId___$annotationId" - def getAnnotation(annotationId: String): Fox[AnnotationProto] = annotationStore.get(annotationId).toFox + def getAnnotation(annotationId: ObjectId): Fox[AnnotationProto] = annotationStore.get(annotationId.toString).toFox def getVolume(tracingId: String): Fox[VolumeTracing] = volumeStore.get(tracingId).toFox @@ -80,8 +81,8 @@ class TemporaryTracingService @Inject()(skeletonStore: TemporaryTracingStore[Ske Fox.successful(()) } - def saveAnnotationProto(annotationId: String, annotationProto: AnnotationProto): Fox[Unit] = { - annotationStore.insert(annotationId, annotationProto, Some(temporaryStoreTimeout)) + def saveAnnotationProto(annotationId: ObjectId, annotationProto: AnnotationProto): Fox[Unit] = { + annotationStore.insert(annotationId.toString, annotationProto, Some(temporaryStoreTimeout)) registerAnnotationId(annotationId) Fox.successful(()) } @@ -93,7 +94,7 @@ class TemporaryTracingService @Inject()(skeletonStore: TemporaryTracingStore[Ske Fox.successful(()) } - def isTemporaryAnnotation(annotationId: String): Fox[Boolean] = + def isTemporaryAnnotation(annotationId: ObjectId): Fox[Boolean] = temporaryTracingIdStore.contains(temporaryAnnotationIdKey(annotationId)) def isTemporaryTracing(tracingId: String): Fox[Boolean] = @@ -107,7 +108,7 @@ class TemporaryTracingService @Inject()(skeletonStore: TemporaryTracingStore[Ske private def registerTracingId(tracingId: String) = temporaryTracingIdStore.insertKey(temporaryTracingIdKey(tracingId), Some(temporaryIdStoreTimeout)) - private def registerAnnotationId(annotationId: String) = + private def registerAnnotationId(annotationId: ObjectId) = temporaryTracingIdStore.insertKey(temporaryAnnotationIdKey(annotationId), Some(temporaryIdStoreTimeout)) } diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/editablemapping/EditableMappingLayer.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/editablemapping/EditableMappingLayer.scala index c245b13dfa5..d822ab2f7e2 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/editablemapping/EditableMappingLayer.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/editablemapping/EditableMappingLayer.scala @@ -3,6 +3,7 @@ package com.scalableminds.webknossos.tracingstore.tracings.editablemapping import com.scalableminds.util.accesscontext.TokenContext import com.scalableminds.util.cache.AlfuCache import com.scalableminds.util.geometry.{BoundingBox, Vec3Int} +import com.scalableminds.util.objectid.ObjectId import com.scalableminds.util.tools.{Fox, FoxImplicits} import com.scalableminds.webknossos.datastore.VolumeTracing.VolumeTracing import com.scalableminds.webknossos.datastore.dataformats.{BucketProvider, MagLocator} @@ -15,9 +16,9 @@ import com.scalableminds.webknossos.datastore.models.datasource.{ DataFormat, DataLayer, DataSourceId, + DatasetLayerAttachments, ElementClass, - SegmentationLayer, - DatasetLayerAttachments + SegmentationLayer } import ucar.ma2.{Array => MultiArray} import com.scalableminds.webknossos.datastore.models.requests.DataReadInstruction @@ -77,7 +78,7 @@ case class EditableMappingLayer(name: String, // set to tracing id largestSegmentId: Option[Long], elementClass: ElementClass.Value, tracing: VolumeTracing, - annotationId: String, + annotationId: ObjectId, annotationService: TSAnnotationService, editableMappingService: EditableMappingService) extends SegmentationLayer { diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/editablemapping/EditableMappingMergeService.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/editablemapping/EditableMappingMergeService.scala index 7a3bd79e56b..8ee4a91f344 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/editablemapping/EditableMappingMergeService.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/editablemapping/EditableMappingMergeService.scala @@ -2,6 +2,7 @@ package com.scalableminds.webknossos.tracingstore.tracings.editablemapping import collections.SequenceUtils import com.scalableminds.util.accesscontext.TokenContext +import com.scalableminds.util.objectid.ObjectId import com.scalableminds.util.time.Instant import com.scalableminds.util.tools.{Fox, FoxImplicits} import com.scalableminds.webknossos.datastore.EditableMappingInfo.EditableMappingInfo @@ -37,9 +38,9 @@ class EditableMappingMergeService @Inject()(val tracingDataStore: TracingDataSto * So that it itself can be merged again. * The earliestAccessibleVersion property ensures that the fully merged annotation is still the earliest accessible one. */ - def mergeEditableMappings(annotationIds: List[String], - firstVolumeAnnotationIdOpt: Option[String], - newAnnotationId: String, + def mergeEditableMappings(annotationIds: List[ObjectId], + firstVolumeAnnotationIdOpt: Option[ObjectId], + newAnnotationId: ObjectId, newVolumeTracingId: String, tracingsWithIds: List[(VolumeTracing, String)], toTemporaryStore: Boolean)(implicit ec: ExecutionContext, tc: TokenContext): Fox[Long] = @@ -68,7 +69,9 @@ class EditableMappingMergeService @Inject()(val tracingDataStore: TracingDataSto Fox .serialCombined(linearizedEditableMappingUpdates) { update: UpdateAction => for { - _ <- annotationUpdatesPutBuffer.put(newAnnotationId, Json.toJson(List(update)), Some(updateVersion)) + _ <- annotationUpdatesPutBuffer.put(newAnnotationId.toString, + Json.toJson(List(update)), + Some(updateVersion)) _ = updateVersion += 1 } yield () } @@ -98,13 +101,13 @@ class EditableMappingMergeService @Inject()(val tracingDataStore: TracingDataSto Fox.failure("Cannot merge annotations with and without editable mappings") } - private def mergeEditableMappingUpdates(annotationIds: List[String], newTracingId: String)( + private def mergeEditableMappingUpdates(annotationIds: List[ObjectId], newTracingId: String)( implicit ec: ExecutionContext): Fox[List[EditableMappingUpdateAction]] = for { updatesByAnnotation <- Fox.serialCombined(annotationIds) { annotationId => for { - updateGroups <- tracingDataStore.annotationUpdates.getMultipleVersionsAsVersionValueTuple(annotationId)( - fromJsonBytes[List[UpdateAction]]) + updateGroups <- tracingDataStore.annotationUpdates.getMultipleVersionsAsVersionValueTuple( + annotationId.toString)(fromJsonBytes[List[UpdateAction]]) updatesIroned: Seq[UpdateAction] = ironOutReverts(updateGroups) editableMappingUpdates = updatesIroned.flatMap { case a: EditableMappingUpdateAction => Some(a.withActionTracingId(newTracingId)) diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/editablemapping/EditableMappingUpdater.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/editablemapping/EditableMappingUpdater.scala index 5f084769048..792d0c557f4 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/editablemapping/EditableMappingUpdater.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/editablemapping/EditableMappingUpdater.scala @@ -1,6 +1,7 @@ package com.scalableminds.webknossos.tracingstore.tracings.editablemapping import com.scalableminds.util.accesscontext.TokenContext +import com.scalableminds.util.objectid.ObjectId import com.scalableminds.util.tools.{Fox, FoxImplicits} import com.scalableminds.webknossos.datastore.AgglomerateGraph.{AgglomerateEdge, AgglomerateGraph} import com.scalableminds.webknossos.datastore.EditableMappingInfo.EditableMappingInfo @@ -32,7 +33,7 @@ import scala.jdk.CollectionConverters.CollectionHasAsScala // this results in only one version increment in the db per update group class EditableMappingUpdater( - annotationId: String, + annotationId: ObjectId, tracingId: String, baseMappingName: String, oldVersion: Long, diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/TSFullMeshService.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/TSFullMeshService.scala index 317df386a24..2e737502d84 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/TSFullMeshService.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/TSFullMeshService.scala @@ -2,6 +2,7 @@ package com.scalableminds.webknossos.tracingstore.tracings.volume import com.scalableminds.util.accesscontext.TokenContext import com.scalableminds.util.geometry.Vec3Int +import com.scalableminds.util.objectid.ObjectId import com.scalableminds.util.time.Instant import com.scalableminds.util.tools.{Fox, FoxImplicits} import com.scalableminds.webknossos.datastore.VolumeTracing.VolumeTracing @@ -36,7 +37,7 @@ class TSFullMeshService @Inject()(volumeTracingService: VolumeTracingService, with FoxImplicits with LazyLogging { - def loadFor(annotationId: String, tracingId: String, fullMeshRequest: FullMeshRequest)( + def loadFor(annotationId: ObjectId, tracingId: String, fullMeshRequest: FullMeshRequest)( implicit ec: ExecutionContext, tc: TokenContext): Fox[Array[Byte]] = for { @@ -47,7 +48,7 @@ class TSFullMeshService @Inject()(volumeTracingService: VolumeTracingService, } yield data private def loadFullMeshFromMeshfile( - annotationId: String, + annotationId: ObjectId, tracingId: String, tracing: VolumeTracing, fullMeshRequest: FullMeshRequest)(implicit ec: ExecutionContext, tc: TokenContext): Fox[Array[Byte]] = @@ -63,7 +64,7 @@ class TSFullMeshService @Inject()(volumeTracingService: VolumeTracingService, } yield array private def loadFullMeshFromAdHoc( - annotationId: String, + annotationId: ObjectId, tracingId: String, tracing: VolumeTracing, fullMeshRequest: FullMeshRequest)(implicit ec: ExecutionContext, tc: TokenContext): Fox[Array[Byte]] = @@ -91,7 +92,7 @@ class TSFullMeshService @Inject()(volumeTracingService: VolumeTracingService, } yield array private def getAllAdHocChunksWithSegmentIndex( - annotationId: String, + annotationId: ObjectId, tracingId: String, tracing: VolumeTracing, mag: Vec3Int, @@ -133,7 +134,7 @@ class TSFullMeshService @Inject()(volumeTracingService: VolumeTracingService, allVertices = vertexChunksWithNeighbors.map(_._1) } yield allVertices - private def getAllAdHocChunksWithNeighborLogic(annotationId: String, + private def getAllAdHocChunksWithNeighborLogic(annotationId: ObjectId, tracingId: String, tracing: VolumeTracing, mag: Vec3Int, @@ -176,7 +177,7 @@ class TSFullMeshService @Inject()(volumeTracingService: VolumeTracingService, } yield allVertices private def loadMeshChunkFromAdHoc( - annotationId: String, + annotationId: ObjectId, tracingId: String, tracing: VolumeTracing, adHocMeshRequest: WebknossosAdHocMeshRequest)(implicit tc: TokenContext): Fox[(Array[Float], List[Int])] = diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeSegmentStatisticsService.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeSegmentStatisticsService.scala index 8cd2cde738c..7f447221b83 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeSegmentStatisticsService.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeSegmentStatisticsService.scala @@ -2,6 +2,7 @@ package com.scalableminds.webknossos.tracingstore.tracings.volume import com.scalableminds.util.accesscontext.TokenContext import com.scalableminds.util.geometry.{BoundingBox, Vec3Int} +import com.scalableminds.util.objectid.ObjectId import com.scalableminds.util.tools.Fox import com.scalableminds.webknossos.datastore.geometry.Vec3IntProto import com.scalableminds.webknossos.datastore.helpers.{NativeBucketScanner, ProtoGeometryImplicits, SegmentStatistics} @@ -26,7 +27,7 @@ class VolumeSegmentStatisticsService @Inject()(volumeTracingService: VolumeTraci protected lazy val bucketScanner = new NativeBucketScanner() // Returns the segment volume (=number of voxels) in the target mag - def getSegmentVolume(annotationId: String, + def getSegmentVolume(annotationId: ObjectId, tracingId: String, segmentId: Long, mag: Vec3Int, @@ -41,7 +42,7 @@ class VolumeSegmentStatisticsService @Inject()(volumeTracingService: VolumeTraci getDataForBucketPositions(annotationId, tracingId) ) - def getSegmentBoundingBox(annotationId: String, + def getSegmentBoundingBox(annotationId: ObjectId, tracingId: String, segmentId: Long, mag: Vec3Int, @@ -57,7 +58,7 @@ class VolumeSegmentStatisticsService @Inject()(volumeTracingService: VolumeTraci getDataForBucketPositions(annotationId, tracingId) ) - private def getDataForBucketPositions(annotationId: String, tracingId: String)( + private def getDataForBucketPositions(annotationId: ObjectId, tracingId: String)( bucketPositions: Seq[Vec3Int], mag: Vec3Int, additionalCoordinates: Option[Seq[AdditionalCoordinate]])( @@ -87,7 +88,7 @@ class VolumeSegmentStatisticsService @Inject()(volumeTracingService: VolumeTraci includeFallbackDataIfAvailable = true) } yield (bucketDataBoxes, elementClassFromProto(tracing.elementClass)) - private def getBucketPositions(annotationId: String, + private def getBucketPositions(annotationId: ObjectId, tracingId: String, mappingName: Option[String], additionalCoordinates: Option[Seq[AdditionalCoordinate]])( diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingLayer.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingLayer.scala index eb1bb302c9a..67a4c8528e6 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingLayer.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingLayer.scala @@ -3,6 +3,7 @@ package com.scalableminds.webknossos.tracingstore.tracings.volume import com.scalableminds.util.accesscontext.TokenContext import com.scalableminds.util.cache.AlfuCache import com.scalableminds.util.geometry.{BoundingBox, Vec3Int} +import com.scalableminds.util.objectid.ObjectId import com.scalableminds.util.tools.{Fox, FoxImplicits} import com.scalableminds.webknossos.datastore.VolumeTracing.VolumeTracing import com.scalableminds.webknossos.datastore.dataformats.{BucketProvider, MagLocator} @@ -76,7 +77,7 @@ class TemporaryVolumeTracingBucketProvider(layer: VolumeTracingLayer)(implicit v case class VolumeTracingLayer( name: String, - annotationId: String, + annotationId: ObjectId, volumeTracingService: VolumeTracingService, temporaryTracingService: TemporaryTracingService, isTemporaryTracing: Boolean = false, diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingService.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingService.scala index 5e22566d5bf..3c663216e8f 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingService.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/tracings/volume/VolumeTracingService.scala @@ -6,6 +6,7 @@ import com.scalableminds.util.cache.AlfuCache import com.scalableminds.util.geometry.{BoundingBox, Vec3Double, Vec3Int} import com.scalableminds.util.io.{NamedStream, ZipIO} import com.scalableminds.util.mvc.Formatter +import com.scalableminds.util.objectid.ObjectId import com.scalableminds.util.time.Instant import com.scalableminds.util.tools.{Fox, FoxImplicits} import com.scalableminds.webknossos.datastore.VolumeTracing.VolumeTracing @@ -75,8 +76,8 @@ class VolumeTracingService @Inject()( adHocMeshServiceHolder.tracingStoreAdHocMeshConfig = (binaryDataService, 30 seconds, 1) val adHocMeshService: AdHocMeshService = adHocMeshServiceHolder.tracingStoreAdHocMeshService - // (tracingId, fallbackLayerNameOpt, userTokenOpt) → remoteFallbackLayerOpt - private val fallbackLayerCache: AlfuCache[(String, Option[String], Option[String]), Option[RemoteFallbackLayer]] = + // (annotationId, fallbackLayerNameOpt, userTokenOpt) → remoteFallbackLayerOpt + private val fallbackLayerCache: AlfuCache[(ObjectId, Option[String], Option[String]), Option[RemoteFallbackLayer]] = AlfuCache(maxCapacity = 100) def saveVolume(tracingId: String, @@ -102,7 +103,7 @@ class VolumeTracingService @Inject()( editableMappingTracingId) ?~> "volumeSegmentIndex.update.failed" def applyBucketMutatingActions(tracingId: String, - annotationId: String, + annotationId: ObjectId, tracing: VolumeTracing, updateActions: List[BucketMutatingVolumeUpdateAction], newVersion: Long)(implicit tc: TokenContext): Fox[Unit] = @@ -190,7 +191,7 @@ class VolumeTracingService @Inject()( else Fox.successful(tracing.mappingName) private def deleteSegmentData(tracingId: String, - annotationId: String, + annotationId: ObjectId, volumeTracing: VolumeTracing, a: DeleteSegmentDataVolumeAction, segmentIndexBuffer: VolumeSegmentIndexBuffer, @@ -257,7 +258,7 @@ class VolumeTracingService @Inject()( } def revertVolumeData(tracingId: String, - annotationId: String, + annotationId: ObjectId, sourceVersion: Long, sourceTracing: VolumeTracing, newVersion: Long, @@ -325,7 +326,7 @@ class VolumeTracingService @Inject()( } yield () } - def initializeWithDataMultiple(annotationId: String, tracingId: String, tracing: VolumeTracing, initialData: File)( + def initializeWithDataMultiple(annotationId: ObjectId, tracingId: String, tracing: VolumeTracing, initialData: File)( implicit mp: MessagesProvider, tc: TokenContext): Fox[Set[Vec3Int]] = if (tracing.version != 0L) @@ -396,7 +397,7 @@ class VolumeTracingService @Inject()( } yield mags } - def initializeWithData(annotationId: String, + def initializeWithData(annotationId: ObjectId, tracingId: String, tracing: VolumeTracing, initialData: File, @@ -448,7 +449,7 @@ class VolumeTracingService @Inject()( } } - def allDataZip(annotationId: String, + def allDataZip(annotationId: ObjectId, tracingId: String, tracing: VolumeTracing, volumeDataZipFormat: VolumeDataZipFormat, @@ -458,7 +459,7 @@ class VolumeTracingService @Inject()( allDataToOutputStream(annotationId, tracingId, tracing, volumeDataZipFormat, voxelSize, os).map(_ => zipped) } - private def allDataToOutputStream(annotationId: String, + private def allDataToOutputStream(annotationId: ObjectId, tracingId: String, tracing: VolumeTracing, volumeDataZipFormmat: VolumeDataZipFormat, @@ -488,7 +489,7 @@ class VolumeTracingService @Inject()( zipResult } - def data(annotationId: String, + def data(annotationId: ObjectId, tracingId: String, tracing: VolumeTracing, dataRequests: DataRequestCollection, @@ -506,7 +507,7 @@ class VolumeTracingService @Inject()( } yield data def dataBucketBoxes( - annotationId: String, + annotationId: ObjectId, tracingId: String, tracing: VolumeTracing, dataRequests: DataRequestCollection, @@ -523,7 +524,7 @@ class VolumeTracingService @Inject()( data <- binaryDataService.handleMultipleBucketRequests(requests) } yield data - def adaptVolumeForDuplicate(sourceAnnotationId: String, + def adaptVolumeForDuplicate(sourceAnnotationId: ObjectId, newTracingId: String, sourceTracing: VolumeTracing, isFromTask: Boolean, @@ -571,10 +572,10 @@ class VolumeTracingService @Inject()( case _ => tracing } - def duplicateVolumeData(sourceAnnotationId: String, + def duplicateVolumeData(sourceAnnotationId: ObjectId, sourceTracingId: String, sourceTracing: VolumeTracing, - newAnnotationId: String, + newAnnotationId: ObjectId, newTracingId: String, newTracing: VolumeTracing)(implicit tc: TokenContext): Fox[Unit] = { var bucketCount = 0 @@ -632,7 +633,7 @@ class VolumeTracingService @Inject()( } private def volumeTracingLayer( - annotationId: String, + annotationId: ObjectId, tracingId: String, tracing: VolumeTracing, isTemporaryTracing: Boolean = false, @@ -660,7 +661,7 @@ class VolumeTracingService @Inject()( def volumeBucketsAreEmpty(tracingId: String): Boolean = volumeDataStore.getMultipleKeys(None, Some(tracingId), limit = Some(1))(toBox).isEmpty - def createAdHocMesh(annotationId: String, + def createAdHocMesh(annotationId: ObjectId, tracingId: String, tracing: VolumeTracing, request: WebknossosAdHocMeshRequest)(implicit tc: TokenContext): Fox[(Array[Float], List[Int])] = @@ -686,7 +687,7 @@ class VolumeTracingService @Inject()( result <- adHocMeshService.requestAdHocMeshViaActor(adHocMeshRequest) } yield result - def findData(annotationId: String, tracingId: String, tracing: VolumeTracing)( + def findData(annotationId: ObjectId, tracingId: String, tracing: VolumeTracing)( implicit tc: TokenContext): Fox[Option[Vec3Int]] = for { _ <- Fox.successful(()) @@ -776,7 +777,7 @@ class VolumeTracingService @Inject()( } def mergeVolumeData( - firstVolumeAnnotationIdOpt: Option[String], + firstVolumeAnnotationIdOpt: Option[ObjectId], volumeTracingIds: Seq[String], volumeTracings: List[VolumeTracing], newVolumeTracingId: String, @@ -784,7 +785,7 @@ class VolumeTracingService @Inject()( toTemporaryStore: Boolean)(implicit mp: MessagesProvider, tc: TokenContext): Fox[MergedVolumeStats] = { val before = Instant.now val volumeLayers = volumeTracingIds.zip(volumeTracings).map { - case (tracingId, tracing) => volumeTracingLayer("annotationIdUnusedInThisContext", tracingId, tracing) + case (tracingId, tracing) => volumeTracingLayer(ObjectId("annotationIdUnusedInThisContext"), tracingId, tracing) } val elementClassProto = volumeLayers.headOption.map(_.tracing.elementClass).getOrElse(ElementClassProto.uint8) @@ -878,7 +879,7 @@ class VolumeTracingService @Inject()( } } - def importVolumeData(annotationId: String, + def importVolumeData(annotationId: ObjectId, tracingId: String, tracing: VolumeTracing, zipFile: File, @@ -944,12 +945,12 @@ class VolumeTracingService @Inject()( } } - def getFallbackLayer(annotationId: String, tracing: VolumeTracing)( + def getFallbackLayer(annotationId: ObjectId, tracing: VolumeTracing)( implicit tc: TokenContext): Fox[Option[RemoteFallbackLayer]] = fallbackLayerCache.getOrLoad((annotationId, tracing.fallbackLayer, tc.userTokenOpt), t => getFallbackLayerFromWebknossos(t._1, t._2)) - private def getFallbackLayerFromWebknossos(annotationId: String, fallbackLayerName: Option[String])( + private def getFallbackLayerFromWebknossos(annotationId: ObjectId, fallbackLayerName: Option[String])( implicit tc: TokenContext): Fox[Option[RemoteFallbackLayer]] = for { dataSource <- remoteWebknossosClient.getDataSourceForAnnotation(annotationId) diff --git a/webknossos-tracingstore/conf/com.scalableminds.webknossos.tracingstore.routes b/webknossos-tracingstore/conf/com.scalableminds.webknossos.tracingstore.routes index 573198f1f3d..0eeebae0092 100644 --- a/webknossos-tracingstore/conf/com.scalableminds.webknossos.tracingstore.routes +++ b/webknossos-tracingstore/conf/com.scalableminds.webknossos.tracingstore.routes @@ -5,25 +5,25 @@ GET /health @com.scalableminds.webknossos.tracingstore.controllers.Application.health # Annotations (concerns AnnotationProto, not annotation info as stored in postgres) -POST /annotation/save @com.scalableminds.webknossos.tracingstore.controllers.TSAnnotationController.save(annotationId: String) -GET /annotation/:annotationId @com.scalableminds.webknossos.tracingstore.controllers.TSAnnotationController.get(annotationId: String, version: Option[Long]) -POST /annotation/:annotationId/update @com.scalableminds.webknossos.tracingstore.controllers.TSAnnotationController.update(annotationId: String) -GET /annotation/:annotationId/updateActionLog @com.scalableminds.webknossos.tracingstore.controllers.TSAnnotationController.updateActionLog(annotationId: String, newestVersion: Option[Long], oldestVersion: Option[Long]) -GET /annotation/:annotationId/newestVersion @com.scalableminds.webknossos.tracingstore.controllers.TSAnnotationController.newestVersion(annotationId: String) -POST /annotation/:annotationId/duplicate @com.scalableminds.webknossos.tracingstore.controllers.TSAnnotationController.duplicate(annotationId: String, newAnnotationId: String, version: Option[Long], isFromTask: Boolean, datasetBoundingBox: Option[String]) -POST /annotation/:annotationId/resetToBase @com.scalableminds.webknossos.tracingstore.controllers.TSAnnotationController.resetToBase(annotationId: String) -POST /annotation/mergedFromIds @com.scalableminds.webknossos.tracingstore.controllers.TSAnnotationController.mergedFromIds(toTemporaryStore: Boolean, newAnnotationId: String) +POST /annotation/save @com.scalableminds.webknossos.tracingstore.controllers.TSAnnotationController.save(annotationId: ObjectId) +GET /annotation/:annotationId @com.scalableminds.webknossos.tracingstore.controllers.TSAnnotationController.get(annotationId: ObjectId, version: Option[Long]) +POST /annotation/:annotationId/update @com.scalableminds.webknossos.tracingstore.controllers.TSAnnotationController.update(annotationId: ObjectId) +GET /annotation/:annotationId/updateActionLog @com.scalableminds.webknossos.tracingstore.controllers.TSAnnotationController.updateActionLog(annotationId: ObjectId, newestVersion: Option[Long], oldestVersion: Option[Long]) +GET /annotation/:annotationId/newestVersion @com.scalableminds.webknossos.tracingstore.controllers.TSAnnotationController.newestVersion(annotationId: ObjectId) +POST /annotation/:annotationId/duplicate @com.scalableminds.webknossos.tracingstore.controllers.TSAnnotationController.duplicate(annotationId: ObjectId, newAnnotationId: ObjectId, version: Option[Long], isFromTask: Boolean, datasetBoundingBox: Option[String]) +POST /annotation/:annotationId/resetToBase @com.scalableminds.webknossos.tracingstore.controllers.TSAnnotationController.resetToBase(annotationId: ObjectId) +POST /annotation/mergedFromIds @com.scalableminds.webknossos.tracingstore.controllers.TSAnnotationController.mergedFromIds(toTemporaryStore: Boolean, newAnnotationId: ObjectId) # Volume tracings POST /volume/save @com.scalableminds.webknossos.tracingstore.controllers.VolumeTracingController.save(newTracingId: String) -POST /volume/:tracingId/initialData @com.scalableminds.webknossos.tracingstore.controllers.VolumeTracingController.initialData(annotationId: String, tracingId: String, minMag: Option[Int], maxMag: Option[Int]) -POST /volume/:tracingId/initialDataMultiple @com.scalableminds.webknossos.tracingstore.controllers.VolumeTracingController.initialDataMultiple(annotationId: String, tracingId: String) -GET /volume/:tracingId @com.scalableminds.webknossos.tracingstore.controllers.VolumeTracingController.get(tracingId: String, annotationId: String, version: Option[Long]) -GET /volume/:tracingId/allDataZip @com.scalableminds.webknossos.tracingstore.controllers.VolumeTracingController.allDataZip(tracingId: String, annotationId: Option[String], version: Option[Long], volumeDataZipFormat: String, voxelSize: Option[String], voxelSizeUnit: Option[String]) -POST /volume/:tracingId/data @com.scalableminds.webknossos.tracingstore.controllers.VolumeTracingController.data(tracingId: String, annotationId: String) +POST /volume/:tracingId/initialData @com.scalableminds.webknossos.tracingstore.controllers.VolumeTracingController.initialData(annotationId: ObjectId, tracingId: String, minMag: Option[Int], maxMag: Option[Int]) +POST /volume/:tracingId/initialDataMultiple @com.scalableminds.webknossos.tracingstore.controllers.VolumeTracingController.initialDataMultiple(annotationId: ObjectId, tracingId: String) +GET /volume/:tracingId @com.scalableminds.webknossos.tracingstore.controllers.VolumeTracingController.get(tracingId: String, annotationId: ObjectId, version: Option[Long]) +GET /volume/:tracingId/allDataZip @com.scalableminds.webknossos.tracingstore.controllers.VolumeTracingController.allDataZip(tracingId: String, annotationId: Option[ObjectId], version: Option[Long], volumeDataZipFormat: String, voxelSize: Option[String], voxelSizeUnit: Option[String]) +POST /volume/:tracingId/data @com.scalableminds.webknossos.tracingstore.controllers.VolumeTracingController.data(tracingId: String, annotationId: ObjectId) POST /volume/:tracingId/adHocMesh @com.scalableminds.webknossos.tracingstore.controllers.VolumeTracingController.requestAdHocMesh(tracingId: String) POST /volume/:tracingId/fullMesh.stl @com.scalableminds.webknossos.tracingstore.controllers.VolumeTracingController.loadFullMeshStl(tracingId: String) -POST /volume/:tracingId/duplicate @com.scalableminds.webknossos.tracingstore.controllers.VolumeTracingController.duplicate(tracingId: String, newAnnotationId: String, newTracingId: String, minMag: Option[Int], maxMag: Option[Int], editPosition: Option[String], editRotation: Option[String], boundingBox: Option[String]) +POST /volume/:tracingId/duplicate @com.scalableminds.webknossos.tracingstore.controllers.VolumeTracingController.duplicate(tracingId: String, newAnnotationId: ObjectId, newTracingId: String, minMag: Option[Int], maxMag: Option[Int], editPosition: Option[String], editRotation: Option[String], boundingBox: Option[String]) POST /volume/:tracingId/segmentIndex/:segmentId @com.scalableminds.webknossos.tracingstore.controllers.VolumeTracingController.getSegmentIndex(tracingId: String, segmentId: Long) POST /volume/:tracingId/importVolumeData @com.scalableminds.webknossos.tracingstore.controllers.VolumeTracingController.importVolumeData(tracingId: String) GET /volume/:tracingId/findData @com.scalableminds.webknossos.tracingstore.controllers.VolumeTracingController.findData(tracingId: String) @@ -33,9 +33,9 @@ POST /volume/getMultiple POST /volume/mergedFromContents @com.scalableminds.webknossos.tracingstore.controllers.VolumeTracingController.mergedFromContents(newTracingId: String) # Editable Mappings -GET /mapping/:tracingId/info @com.scalableminds.webknossos.tracingstore.controllers.EditableMappingController.editableMappingInfo(tracingId: String, annotationId: String, version: Option[Long]) +GET /mapping/:tracingId/info @com.scalableminds.webknossos.tracingstore.controllers.EditableMappingController.editableMappingInfo(tracingId: String, annotationId: ObjectId, version: Option[Long]) GET /mapping/:tracingId/segmentsForAgglomerate @com.scalableminds.webknossos.tracingstore.controllers.EditableMappingController.segmentIdsForAgglomerate(tracingId: String, agglomerateId: Long) -POST /mapping/:tracingId/agglomeratesForSegments @com.scalableminds.webknossos.tracingstore.controllers.EditableMappingController.agglomerateIdsForSegments(tracingId: String, annotationId: String, version: Option[Long]) +POST /mapping/:tracingId/agglomeratesForSegments @com.scalableminds.webknossos.tracingstore.controllers.EditableMappingController.agglomerateIdsForSegments(tracingId: String, annotationId: ObjectId, version: Option[Long]) POST /mapping/:tracingId/agglomerateGraphMinCut @com.scalableminds.webknossos.tracingstore.controllers.EditableMappingController.agglomerateGraphMinCut(tracingId: String) POST /mapping/:tracingId/agglomerateGraphNeighbors @com.scalableminds.webknossos.tracingstore.controllers.EditableMappingController.agglomerateGraphNeighbors(tracingId: String) GET /mapping/:tracingId/agglomerateSkeleton/:agglomerateId @com.scalableminds.webknossos.tracingstore.controllers.EditableMappingController.agglomerateSkeleton(tracingId: String, agglomerateId: Long) @@ -70,6 +70,6 @@ GET /volume/zarr3_experimental/:tracingId/:mag/:coordinates POST /skeleton/save @com.scalableminds.webknossos.tracingstore.controllers.SkeletonTracingController.save(newTracingId: String) POST /skeleton/saveMultiple @com.scalableminds.webknossos.tracingstore.controllers.SkeletonTracingController.saveMultiple() POST /skeleton/mergedFromContents @com.scalableminds.webknossos.tracingstore.controllers.SkeletonTracingController.mergedFromContents(newTracingId: String) -GET /skeleton/:tracingId @com.scalableminds.webknossos.tracingstore.controllers.SkeletonTracingController.get(tracingId: String, annotationId: String, version: Option[Long]) +GET /skeleton/:tracingId @com.scalableminds.webknossos.tracingstore.controllers.SkeletonTracingController.get(tracingId: String, annotationId: ObjectId, version: Option[Long]) POST /skeleton/:tracingId/duplicate @com.scalableminds.webknossos.tracingstore.controllers.SkeletonTracingController.duplicate(tracingId: String, newTracingId: String, editPosition: Option[String], editRotation: Option[String], boundingBox: Option[String]) POST /skeleton/getMultiple @com.scalableminds.webknossos.tracingstore.controllers.SkeletonTracingController.getMultiple From 1b3264e3097006b7d847126c47a6e8201b293063 Mon Sep 17 00:00:00 2001 From: Florian M Date: Thu, 5 Jun 2025 15:44:54 +0200 Subject: [PATCH 2/7] format --- .../controllers/TSAnnotationController.scala | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/controllers/TSAnnotationController.scala b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/controllers/TSAnnotationController.scala index 0622a5bb909..55a7fe1600d 100644 --- a/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/controllers/TSAnnotationController.scala +++ b/webknossos-tracingstore/app/com/scalableminds/webknossos/tracingstore/controllers/TSAnnotationController.scala @@ -5,12 +5,21 @@ import com.google.inject.Inject import com.scalableminds.util.geometry.BoundingBox import com.scalableminds.util.objectid.ObjectId import com.scalableminds.util.tools.Fox -import com.scalableminds.webknossos.datastore.Annotation.{AnnotationLayerProto, AnnotationLayerTypeProto, AnnotationProto} +import com.scalableminds.webknossos.datastore.Annotation.{ + AnnotationLayerProto, + AnnotationLayerTypeProto, + AnnotationProto +} import com.scalableminds.webknossos.datastore.controllers.Controller import com.scalableminds.webknossos.datastore.models.annotation.AnnotationLayer import com.scalableminds.webknossos.datastore.services.UserAccessRequest import com.scalableminds.webknossos.tracingstore.TracingStoreAccessTokenService -import com.scalableminds.webknossos.tracingstore.annotation.{AnnotationTransactionService, ResetToBaseAnnotationAction, TSAnnotationService, UpdateActionGroup} +import com.scalableminds.webknossos.tracingstore.annotation.{ + AnnotationTransactionService, + ResetToBaseAnnotationAction, + TSAnnotationService, + UpdateActionGroup +} import com.scalableminds.webknossos.tracingstore.slacknotification.TSSlackNotificationService import com.scalableminds.webknossos.tracingstore.tracings._ import com.scalableminds.webknossos.tracingstore.tracings.editablemapping.EditableMappingMergeService From 6903c94bda3001297a7e73d772802a013ce143e3 Mon Sep 17 00:00:00 2001 From: Florian M Date: Tue, 17 Jun 2025 13:05:51 +0200 Subject: [PATCH 3/7] adapt test --- test/backend/AnnotationUserStateTestSuite.scala | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/test/backend/AnnotationUserStateTestSuite.scala b/test/backend/AnnotationUserStateTestSuite.scala index 1e33127aa34..a9c5e355810 100644 --- a/test/backend/AnnotationUserStateTestSuite.scala +++ b/test/backend/AnnotationUserStateTestSuite.scala @@ -1,5 +1,6 @@ package backend +import com.scalableminds.util.objectid.ObjectId import com.scalableminds.webknossos.datastore.IdWithBool.{Id32WithBool, Id64WithBool} import com.scalableminds.webknossos.datastore.SkeletonTracing import com.scalableminds.webknossos.tracingstore.tracings.AnnotationUserStateUtils @@ -8,6 +9,10 @@ import org.scalatestplus.play.PlaySpec class AnnotationUserStateTestSuite extends PlaySpec with AnnotationUserStateUtils { + private lazy val userAId = ObjectId("userA") + private lazy val userBId = ObjectId("userB") + private lazy val userCId = ObjectId("userC") + private lazy val dummySkeletonWithUserState = Dummies.skeletonTracing.copy( userStates = Seq( SkeletonTracing.SkeletonUserStateProto( @@ -28,7 +33,7 @@ class AnnotationUserStateTestSuite extends PlaySpec with AnnotationUserStateUtil "Skeleton user state" should { "be rendered into new skeleton user state correctly for userA (sparse user state present for them)" in { val renderedUserState = - renderSkeletonUserStateIntoUserState(dummySkeletonWithUserState, "userA", "userB") + renderSkeletonUserStateIntoUserState(dummySkeletonWithUserState, userAId, userBId) assert(renderedUserState.treeVisibilities == Seq(Id32WithBool(1, false), Id32WithBool(2, true))) assert(renderedUserState.activeNodeId == Some(5)) assert(renderedUserState.treeGroupExpandedStates == Seq(Id32WithBool(1, true))) @@ -36,14 +41,14 @@ class AnnotationUserStateTestSuite extends PlaySpec with AnnotationUserStateUtil "be rendered into new skeleton user state correctly for userB (owner)" in { val renderedUserState = - renderSkeletonUserStateIntoUserState(dummySkeletonWithUserState, "userB", "userB") + renderSkeletonUserStateIntoUserState(dummySkeletonWithUserState, userBId, userBId) assert(renderedUserState.treeVisibilities == Seq(Id32WithBool(1, true), Id32WithBool(2, true))) assert(renderedUserState.treeGroupExpandedStates == Seq.empty) } "be rendered into new skeleton user state correctly for userC (no user state present for them)" in { val renderedUserState = - renderSkeletonUserStateIntoUserState(dummySkeletonWithUserState, "userC", "userB") + renderSkeletonUserStateIntoUserState(dummySkeletonWithUserState, userCId, userBId) assert(renderedUserState.treeVisibilities == Seq(Id32WithBool(1, true), Id32WithBool(2, true))) assert(renderedUserState.activeNodeId == Some(2)) assert(renderedUserState.treeGroupExpandedStates == Seq.empty) @@ -55,14 +60,14 @@ class AnnotationUserStateTestSuite extends PlaySpec with AnnotationUserStateUtil "respect id mapping" in { val tracingAUserStates = Seq( VolumeTracingDefaults - .emptyUserState("userA") + .emptyUserState(userAId) .copy( segmentVisibilities = Seq(Id64WithBool(1L, true)), segmentGroupExpandedStates = Seq(Id32WithBool(1, true)) )) val tracingBUserStates = Seq( VolumeTracingDefaults - .emptyUserState("userA") + .emptyUserState(userAId) .copy( segmentVisibilities = Seq(Id64WithBool(1L, false)), segmentGroupExpandedStates = Seq(Id32WithBool(1, false)) @@ -78,7 +83,7 @@ class AnnotationUserStateTestSuite extends PlaySpec with AnnotationUserStateUtil assert( mergedUserStates == Seq( VolumeTracingDefaults - .emptyUserState("userA") + .emptyUserState(userAId) .copy(segmentVisibilities = Seq(Id64WithBool(1, true), Id64WithBool(2L, false)), segmentGroupExpandedStates = Seq(Id32WithBool(6, true), Id32WithBool(1, false))) )) From 28d632b77abf0179c9f2664ab94bccd1161a321e Mon Sep 17 00:00:00 2001 From: Florian M Date: Thu, 19 Jun 2025 14:38:15 +0200 Subject: [PATCH 4/7] use userAId for the update actions in e2e tests --- frontend/javascripts/test/e2e-setup.ts | 3 +++ frontend/javascripts/test/helpers/saveHelpers.ts | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/javascripts/test/e2e-setup.ts b/frontend/javascripts/test/e2e-setup.ts index 2e671b77bac..6606337ff41 100644 --- a/frontend/javascripts/test/e2e-setup.ts +++ b/frontend/javascripts/test/e2e-setup.ts @@ -34,6 +34,8 @@ const tokenUserFInOrgaX = let currentUserAuthToken = tokenUserA; +const idUserA = "570b9f4d2a7c0e4d008da6ef"; + function setUserAuthToken(token: string) { currentUserAuthToken = token; } @@ -163,4 +165,5 @@ export { tokenUserFInOrgaX, tokenUserFInOrgaY, setUserAuthToken, + idUserA, }; diff --git a/frontend/javascripts/test/helpers/saveHelpers.ts b/frontend/javascripts/test/helpers/saveHelpers.ts index 68be879397d..cb06127b7e9 100644 --- a/frontend/javascripts/test/helpers/saveHelpers.ts +++ b/frontend/javascripts/test/helpers/saveHelpers.ts @@ -2,6 +2,7 @@ import type { TracingStats } from "viewer/model/accessors/annotation_accessor"; import type { UpdateActionWithoutIsolationRequirement } from "viewer/model/sagas/update_actions"; import type { SaveQueueEntry } from "viewer/store"; import dummyUser from "test/fixtures/dummy_user"; +import idUserA from "test/e2e-setup"; export function createSaveQueueFromUpdateActions( updateActions: UpdateActionWithoutIsolationRequirement[][], @@ -15,7 +16,7 @@ export function createSaveQueueFromUpdateActions( actions: ua, info: "[]", transactionGroupCount: 1, - authorId: dummyUser.id, + authorId: idUserA, transactionGroupIndex: 0, transactionId: "dummyRequestId", })); From c4609e214e064f6c0d44a1bc7fe6ff31e12dc986 Mon Sep 17 00:00:00 2001 From: Florian M Date: Thu, 19 Jun 2025 14:40:19 +0200 Subject: [PATCH 5/7] unused import --- frontend/javascripts/test/helpers/saveHelpers.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/javascripts/test/helpers/saveHelpers.ts b/frontend/javascripts/test/helpers/saveHelpers.ts index cb06127b7e9..61f731d3e68 100644 --- a/frontend/javascripts/test/helpers/saveHelpers.ts +++ b/frontend/javascripts/test/helpers/saveHelpers.ts @@ -1,7 +1,6 @@ import type { TracingStats } from "viewer/model/accessors/annotation_accessor"; import type { UpdateActionWithoutIsolationRequirement } from "viewer/model/sagas/update_actions"; import type { SaveQueueEntry } from "viewer/store"; -import dummyUser from "test/fixtures/dummy_user"; import idUserA from "test/e2e-setup"; export function createSaveQueueFromUpdateActions( From dbc10a96f2b6f58185bb180a8e415e464fd23445 Mon Sep 17 00:00:00 2001 From: Florian M Date: Thu, 19 Jun 2025 14:50:31 +0200 Subject: [PATCH 6/7] import with curly braces because javascript --- frontend/javascripts/test/helpers/saveHelpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/javascripts/test/helpers/saveHelpers.ts b/frontend/javascripts/test/helpers/saveHelpers.ts index 61f731d3e68..7e03931587c 100644 --- a/frontend/javascripts/test/helpers/saveHelpers.ts +++ b/frontend/javascripts/test/helpers/saveHelpers.ts @@ -1,7 +1,7 @@ import type { TracingStats } from "viewer/model/accessors/annotation_accessor"; import type { UpdateActionWithoutIsolationRequirement } from "viewer/model/sagas/update_actions"; import type { SaveQueueEntry } from "viewer/store"; -import idUserA from "test/e2e-setup"; +import { idUserA } from "test/e2e-setup"; export function createSaveQueueFromUpdateActions( updateActions: UpdateActionWithoutIsolationRequirement[][], From b9a02379397e6ddd01bd58ca81c6f44cb2b35653 Mon Sep 17 00:00:00 2001 From: Florian M Date: Thu, 19 Jun 2025 15:09:41 +0200 Subject: [PATCH 7/7] use e2e user id only in e2e context --- .../test/backend-snapshot-tests/annotations.e2e.ts | 13 ++++++++++++- frontend/javascripts/test/helpers/saveHelpers.ts | 4 +++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/frontend/javascripts/test/backend-snapshot-tests/annotations.e2e.ts b/frontend/javascripts/test/backend-snapshot-tests/annotations.e2e.ts index 11d1953c88d..31b4532a504 100644 --- a/frontend/javascripts/test/backend-snapshot-tests/annotations.e2e.ts +++ b/frontend/javascripts/test/backend-snapshot-tests/annotations.e2e.ts @@ -199,6 +199,8 @@ describe("Annotation API (E2E)", () => { [UpdateActions.updateCameraAnnotation([2, 3, 4], null, [1, 2, 3], 2)], ], 123456789, + null, + true, ), 0, ); @@ -246,6 +248,8 @@ describe("Annotation API (E2E)", () => { createSaveQueueFromUpdateActions( [createTreesUpdateActions, [updateTreeGroupsUpdateAction]], 123456789, + null, + true, ), 0, ); @@ -284,7 +288,12 @@ describe("Annotation API (E2E)", () => { const updateTreeAction = UpdateActions.updateTree(trees.getOrThrow(1), tracingId); const [saveQueue] = addVersionNumbers( - createSaveQueueFromUpdateActions([createTreesUpdateActions, [updateTreeAction]], 123456789), + createSaveQueueFromUpdateActions( + [createTreesUpdateActions, [updateTreeAction]], + 123456789, + null, + true, + ), 0, ); @@ -301,6 +310,8 @@ describe("Annotation API (E2E)", () => { createSaveQueueFromUpdateActions( [[UpdateActions.updateMetadataOfAnnotation(newDescription)]], 123456789, + null, + true, ), 0, ); diff --git a/frontend/javascripts/test/helpers/saveHelpers.ts b/frontend/javascripts/test/helpers/saveHelpers.ts index 7e03931587c..3ad197c7a23 100644 --- a/frontend/javascripts/test/helpers/saveHelpers.ts +++ b/frontend/javascripts/test/helpers/saveHelpers.ts @@ -2,11 +2,13 @@ import type { TracingStats } from "viewer/model/accessors/annotation_accessor"; import type { UpdateActionWithoutIsolationRequirement } from "viewer/model/sagas/update_actions"; import type { SaveQueueEntry } from "viewer/store"; import { idUserA } from "test/e2e-setup"; +import dummyUser from "test/fixtures/dummy_user"; export function createSaveQueueFromUpdateActions( updateActions: UpdateActionWithoutIsolationRequirement[][], timestamp: number, stats: TracingStats | null = null, + useE2eAuthorId: boolean = false, ): SaveQueueEntry[] { return updateActions.map((ua) => ({ version: -1, @@ -15,7 +17,7 @@ export function createSaveQueueFromUpdateActions( actions: ua, info: "[]", transactionGroupCount: 1, - authorId: idUserA, + authorId: useE2eAuthorId ? idUserA : dummyUser.id, transactionGroupIndex: 0, transactionId: "dummyRequestId", }));