diff --git a/src/Http/Controllers/Api/Projects/FilterImageAnnotationsByLabelController.php b/src/Http/Controllers/Api/Projects/FilterImageAnnotationsByLabelController.php index bf2de73..08b25ad 100644 --- a/src/Http/Controllers/Api/Projects/FilterImageAnnotationsByLabelController.php +++ b/src/Http/Controllers/Api/Projects/FilterImageAnnotationsByLabelController.php @@ -2,10 +2,12 @@ namespace Biigle\Modules\Largo\Http\Controllers\Api\Projects; -use Biigle\Http\Controllers\Api\Controller; -use Biigle\ImageAnnotation; +use Generator; use Biigle\Project; +use Biigle\ImageAnnotation; use Illuminate\Http\Request; +use Biigle\Http\Controllers\Api\Controller; +use Symfony\Component\HttpFoundation\StreamedJsonResponse; class FilterImageAnnotationsByLabelController extends Controller { @@ -49,4 +51,57 @@ public function index(Request $request, $pid, $lid) ->orderBy('image_annotations.id', 'desc') ->pluck('images.uuid', 'image_annotations.id'); } + + /** + * Get all image annotations with uuids for a given project + * + * @api {get} + * @apiGroup Projects + * @apiName test + * @apiParam {Number} id The Project ID + * @apiPermission user + * @apiDescription Returns a stream containing the image uuids and ids of annotations, labels and label trees + * + * @apiSuccessExample {json} Success response: + * [{ + * "uuid":"9198ea9c-ef97-4af7-8018-407d16eafb65", + * "annotation_id":41, + * "label_id":14, + * "label_tree_id":123 + * }] + * + * + * @param int $id Project ID + * @return \Symfony\Component\HttpFoundation\StreamedJsonResponse + */ + public function getProjectsAnnotationLabels($id) + { + $project = Project::findOrFail($id); + $this->authorize('access', $project); + + $annotations = $project->imageVolumes() + ->join('images', 'volumes.id', '=', 'images.volume_id') + ->join('image_annotations', 'images.id', '=', 'image_annotations.image_id') + ->join('image_annotation_labels', 'image_annotations.id', '=', 'image_annotation_labels.annotation_id') + ->join('labels', 'image_annotation_labels.label_id', '=', 'labels.id') + ->select( + 'images.uuid', + 'image_annotations.id as annotation_id', + 'image_annotation_labels.label_id', + 'labels.label_tree_id' + ); + + $res = function () use ($annotations): Generator { + foreach ($annotations->lazy() as $a) { + yield [ + 'uuid' => $a->uuid, + 'annotation_id' => $a->annotation_id, + 'label_id' => $a->label_id, + 'label_tree_id' => $a->label_tree_id + ]; + } + }; + + return new StreamedJsonResponse($res()); + } } diff --git a/src/Http/Controllers/Api/Projects/FilterVideoAnnotationsByLabelController.php b/src/Http/Controllers/Api/Projects/FilterVideoAnnotationsByLabelController.php index 1af6016..4028267 100644 --- a/src/Http/Controllers/Api/Projects/FilterVideoAnnotationsByLabelController.php +++ b/src/Http/Controllers/Api/Projects/FilterVideoAnnotationsByLabelController.php @@ -2,10 +2,12 @@ namespace Biigle\Modules\Largo\Http\Controllers\Api\Projects; -use Biigle\Http\Controllers\Api\Controller; +use Generator; use Biigle\Project; use Biigle\VideoAnnotation; use Illuminate\Http\Request; +use Biigle\Http\Controllers\Api\Controller; +use Symfony\Component\HttpFoundation\StreamedJsonResponse; class FilterVideoAnnotationsByLabelController extends Controller { @@ -49,4 +51,57 @@ public function index(Request $request, $pid, $lid) ->orderBy('video_annotations.id', 'desc') ->pluck('videos.uuid', 'video_annotations.id'); } + + /** + * Get all video annotations with uuids for a given project + * + * @api {get} + * @apiGroup Projects + * @apiName test + * @apiParam {Number} id The Project ID + * @apiPermission user + * @apiDescription Returns a stream containing the video uuids and ids of annotations, labels and label trees + * + * @apiSuccessExample {json} Success response: + * [{ + * "uuid":"9198ea9c-ef97-4af7-8018-407d16eafb65", + * "annotation_id":41, + * "label_id":14, + * "label_tree_id":123 + * }] + * + * + * @param int $id Project ID + * @return \Symfony\Component\HttpFoundation\StreamedJsonResponse + */ + public function getProjectsAnnotationLabels($id) + { + $project = Project::findOrFail($id); + $this->authorize('access', $project); + + $annotations = $project->videoVolumes() + ->join('videos', 'volumes.id', '=', 'videos.volume_id') + ->join('video_annotations', 'videos.id', '=', 'video_annotations.video_id') + ->join('video_annotation_labels', 'video_annotations.id', '=', 'video_annotation_labels.annotation_id') + ->join('labels', 'video_annotation_labels.label_id', '=', 'labels.id') + ->select( + 'videos.uuid', + 'video_annotations.id as annotation_id', + 'video_annotation_labels.label_id', + 'labels.label_tree_id' + ); + + $res = function () use ($annotations): Generator { + foreach ($annotations->lazy() as $a) { + yield [ + 'uuid' => $a->uuid, + 'annotation_id' => $a->annotation_id, + 'label_id' => $a->label_id, + 'label_tree_id' => $a->label_tree_id + ]; + } + }; + + return new StreamedJsonResponse($res()); + } } diff --git a/src/Http/Controllers/Api/Volumes/FilterImageAnnotationsByLabelController.php b/src/Http/Controllers/Api/Volumes/FilterImageAnnotationsByLabelController.php index d6b6c07..3f390c1 100644 --- a/src/Http/Controllers/Api/Volumes/FilterImageAnnotationsByLabelController.php +++ b/src/Http/Controllers/Api/Volumes/FilterImageAnnotationsByLabelController.php @@ -2,10 +2,12 @@ namespace Biigle\Modules\Largo\Http\Controllers\Api\Volumes; -use Biigle\Http\Controllers\Api\Controller; -use Biigle\ImageAnnotation; +use Generator; use Biigle\Volume; +use Biigle\ImageAnnotation; use Illuminate\Http\Request; +use Biigle\Http\Controllers\Api\Controller; +use Symfony\Component\HttpFoundation\StreamedJsonResponse; class FilterImageAnnotationsByLabelController extends Controller { @@ -58,4 +60,55 @@ public function index(Request $request, $vid, $lid) ->orderBy('image_annotations.id', 'desc') ->pluck('images.uuid', 'image_annotations.id'); } + + /** + * Get all image annotations with uuids for a given volume + * + * @api {get} + * @apiGroup Volumes + * @apiName test + * @apiParam {Number} id The Volume ID + * @apiPermission user + * @apiDescription Returns a stream containing the image uuids and ids of annotations, labels and label trees + * + * @apiSuccessExample {json} Success response: + * [{ + * "uuid":"9198ea9c-ef97-4af7-8018-407d16eafb65", + * "annotation_id":41, + * "label_id":14, + * "label_tree_id":123 + * }] + * + * @param int $id Volume ID + * @return \Symfony\Component\HttpFoundation\StreamedJsonResponse + */ + public function getVolumeAnnotationLabels($id) + { + $volume = Volume::findOrFail($id); + $this->authorize('access', $volume); + + $annotations = $volume->images() + ->join('image_annotations', 'images.id', '=', 'image_annotations.image_id') + ->join('image_annotation_labels', 'image_annotations.id', '=', 'image_annotation_labels.annotation_id') + ->join('labels', 'image_annotation_labels.label_id', '=', 'labels.id') + ->select( + 'images.uuid', + 'image_annotations.id as annotation_id', + 'image_annotation_labels.label_id', + 'labels.label_tree_id' + ); + + $res = function () use ($annotations): Generator { + foreach ($annotations->lazy() as $a) { + yield [ + 'uuid' => $a->uuid, + 'annotation_id' => $a->annotation_id, + 'label_id' => $a->label_id, + 'label_tree_id' => $a->label_tree_id + ]; + } + }; + + return new StreamedJsonResponse($res()); + } } diff --git a/src/Http/Controllers/Api/Volumes/FilterVideoAnnotationsByLabelController.php b/src/Http/Controllers/Api/Volumes/FilterVideoAnnotationsByLabelController.php index bafc3f0..3a39fae 100644 --- a/src/Http/Controllers/Api/Volumes/FilterVideoAnnotationsByLabelController.php +++ b/src/Http/Controllers/Api/Volumes/FilterVideoAnnotationsByLabelController.php @@ -2,10 +2,12 @@ namespace Biigle\Modules\Largo\Http\Controllers\Api\Volumes; -use Biigle\Http\Controllers\Api\Controller; -use Biigle\VideoAnnotation; +use Generator; use Biigle\Volume; +use Biigle\VideoAnnotation; use Illuminate\Http\Request; +use Biigle\Http\Controllers\Api\Controller; +use Symfony\Component\HttpFoundation\StreamedJsonResponse; class FilterVideoAnnotationsByLabelController extends Controller { @@ -58,4 +60,56 @@ public function index(Request $request, $vid, $lid) ->orderBy('video_annotations.id', 'desc') ->pluck('videos.uuid', 'video_annotations.id'); } + + /** + * Get all video annotations with uuids for a given volume + * + * @api {get} + * @apiGroup Volumes + * @apiName test + * @apiParam {Number} id The Volume ID + * @apiPermission user + * @apiDescription Returns a stream containing the video uuids and ids of annotations, labels and label trees + * + * @apiSuccessExample {json} Success response: + * [{ + * "uuid":"9198ea9c-ef97-4af7-8018-407d16eafb65", + * "annotation_id":41, + * "label_id":14, + * "label_tree_id":123 + * }] + * + * + * @param int $id Volume ID + * @return \Symfony\Component\HttpFoundation\StreamedJsonResponse + */ + public function getVolumeAnnotationLabels($id) + { + $volume = Volume::findOrFail($id); + $this->authorize('access', $volume); + + $annotations = $volume->videos() + ->join('video_annotations', 'videos.id', '=', 'video_annotations.video_id') + ->join('video_annotation_labels', 'video_annotations.id', '=', 'video_annotation_labels.annotation_id') + ->join('labels', 'video_annotation_labels.label_id', '=', 'labels.id') + ->select( + 'videos.uuid', + 'video_annotations.id as annotation_id', + 'video_annotation_labels.label_id', + 'labels.label_tree_id' + ); + + $res = function () use ($annotations): Generator { + foreach ($annotations->lazy() as $a) { + yield [ + 'uuid' => $a->uuid, + 'annotation_id' => $a->annotation_id, + 'label_id' => $a->label_id, + 'label_tree_id' => $a->label_tree_id + ]; + } + }; + + return new StreamedJsonResponse($res()); + } } diff --git a/src/Http/routes.php b/src/Http/routes.php index 4edf1eb..0490bff 100644 --- a/src/Http/routes.php +++ b/src/Http/routes.php @@ -53,6 +53,14 @@ 'uses' => 'Projects\FilterVideoAnnotationsByLabelController@index', ]); + $router->get('projects/{id}/image-annotations', [ + 'uses' => 'Projects\FilterImageAnnotationsByLabelController@getProjectsAnnotationLabels', + ]); + + $router->get('projects/{id}/video-annotations', [ + 'uses' => 'Projects\FilterVideoAnnotationsByLabelController@getProjectsAnnotationLabels', + ]); + $router->get('volumes/{id}/annotations/sort/outliers/{id2}', [ 'uses' => 'Volumes\SortAnnotationsByOutliersController@index', ]); @@ -76,4 +84,12 @@ $router->get('volumes/{id}/video-annotations/filter/label/{id2}', [ 'uses' => 'Volumes\FilterVideoAnnotationsByLabelController@index', ]); + + $router->get('volume/{id}/image-annotations', [ + 'uses' => 'Volumes\FilterImageAnnotationsByLabelController@getVolumeAnnotationLabels' + ]); + + $router->get('volume/{id}/video-annotations', [ + 'uses' => 'Volumes\FilterVideoAnnotationsByLabelController@getVolumeAnnotationLabels' + ]); }); diff --git a/src/public/assets/scripts/main.js b/src/public/assets/scripts/main.js index 0469167..4328849 100644 --- a/src/public/assets/scripts/main.js +++ b/src/public/assets/scripts/main.js @@ -1 +1 @@ -(()=>{"use strict";var t,e={543:()=>{var t="imageAnnotation",e="videoAnnotation";function n(t,e,n,i,s,o,a,r){var l,u="function"==typeof t?t.options:t;if(e&&(u.render=e,u.staticRenderFns=n,u._compiled=!0),i&&(u.functional=!0),o&&(u._scopeId="data-v-"+o),a?(l=function(t){(t=t||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext)||"undefined"==typeof __VUE_SSR_CONTEXT__||(t=__VUE_SSR_CONTEXT__),s&&s.call(this,t),t&&t._registeredComponents&&t._registeredComponents.add(a)},u._ssrRegister=l):s&&(l=r?function(){s.call(this,(u.functional?this.parent:this).$root.$options.shadowRoot)}:s),l)if(u.functional){u._injectStyles=l;var c=u.render;u.render=function(t,e){return l.call(e),c(t,e)}}else{var d=u.beforeCreate;u.beforeCreate=d?[].concat(d,l):[l]}return{exports:t,options:u}}const i=n({computed:{id:function(){return this.image.id},uuid:function(){return this.image.uuid},type:function(){return this.image.type},patchPrefix:function(){return this.uuid[0]+this.uuid[1]+"/"+this.uuid[2]+this.uuid[3]+"/"+this.uuid},urlTemplate:function(){return biigle.$require("largo.patchUrlTemplate")}},methods:{getThumbnailUrl:function(){return this.type===e?this.urlTemplate.replace(":prefix",this.patchPrefix).replace(":id","v-".concat(this.id)):this.urlTemplate.replace(":prefix",this.patchPrefix).replace(":id",this.id)}},created:function(){this.type===t?this.showAnnotationRoute=biigle.$require("largo.showImageAnnotationRoute"):this.showAnnotationRoute=biigle.$require("largo.showVideoAnnotationRoute")}},undefined,undefined,!1,null,null,null).exports;const s=n({mixins:[i],props:{_id:{type:String,required:!0},_uuid:{type:String,required:!0},label:{type:Object,required:!0},emptySrc:{type:String,required:!0},_urlTemplate:{type:String,required:!0}},data:function(){return{url:""}},computed:{title:function(){return"Example annotation for label "+this.label.name},src:function(){return this.url||this.emptySrc},image:function(){return{id:this._id,uuid:this._uuid,type:t}},urlTemplate:function(){return this._urlTemplate}},methods:{showEmptyImage:function(){this.url=""}},created:function(){this.url=this.getThumbnailUrl()}},undefined,undefined,!1,null,null,null).exports,o=Vue.resource("api/v1/volumes{/id}/largo",{},{queryImageAnnotations:{method:"GET",url:"api/v1/volumes{/id}/image-annotations/filter/label{/label_id}"},queryVideoAnnotations:{method:"GET",url:"api/v1/volumes{/id}/video-annotations/filter/label{/label_id}"},queryExampleAnnotations:{method:"GET",url:"api/v1/volumes{/id}/image-annotations/examples{/label_id}"},sortAnnotationsByOutlier:{method:"GET",url:"api/v1/volumes{/id}/annotations/sort/outliers{/label_id}"},sortAnnotationsBySimilarity:{method:"GET",url:"api/v1/volumes{/id}/annotations/sort/similarity"}});var a=biigle.$require("echo"),r=biigle.$require("events"),l=biigle.$require("messages").handleErrorResponse,u=biigle.$require("volumes.components.imageGrid"),c=biigle.$require("volumes.components.imageGridImage"),d=biigle.$require("annotations.components.labelsTabPlugins"),h=biigle.$require("labelTrees.components.labelTrees"),m=biigle.$require("core.mixins.loader"),g=biigle.$require("messages"),f=biigle.$require("core.components.powerToggle"),p=biigle.$require("core.models.Settings"),v=biigle.$require("annotations.components.settingsTabPlugins"),b=biigle.$require("core.components.sidebar"),y=biigle.$require("core.components.sidebarTab");const S=n({mixins:[m],components:{annotationPatch:s},props:{label:{default:null},volumeId:{type:Number,required:!0},count:{type:Number,default:3}},data:function(){return{exampleLabel:null,exampleAnnotations:[],cache:{},shown:!0}},computed:{isShown:function(){return this.shown&&null!==this.label},hasExamples:function(){return this.exampleLabel&&this.exampleAnnotations&&Object.keys(this.exampleAnnotations).length>0}},methods:{parseResponse:function(t){return t.data},setExampleAnnotations:function(t){(!t[0].hasOwnProperty("annotations")||Object.keys(t[0].annotations).length0){var n={};t.forEach((function(t){n[t.type===e?"v"+t.id:"i"+t.id]=t})),t=this.sortingSequence.map((function(t){return n[t]}))}return this.sortingDirection===$?t.slice().reverse():t},allAnnotations:function(){var t=[];for(var e in this.annotationsCache)this.annotationsCache.hasOwnProperty(e)&&(t=t.concat(this.annotationsCache[e]));return t},hasNoAnnotations:function(){return this.selectedLabel&&!this.loading&&0===this.annotations.length},dismissedAnnotations:function(){return this.allAnnotations.filter((function(t){return t.dismissed}))},annotationsWithNewLabel:function(){return this.dismissedAnnotations.filter((function(t){return!!t.newLabel}))},hasDismissedAnnotations:function(){return this.dismissedAnnotations.length>0},dismissedImageAnnotationsToSave:function(){return this.packDismissedToSave(this.dismissedAnnotations.filter((function(e){return e.type===t})))},dismissedVideoAnnotationsToSave:function(){return this.packDismissedToSave(this.dismissedAnnotations.filter((function(t){return t.type===e})))},changedImageAnnotationsToSave:function(){return this.packChangedToSave(this.annotationsWithNewLabel.filter((function(e){return e.type===t})))},changedVideoAnnotationsToSave:function(){return this.packChangedToSave(this.annotationsWithNewLabel.filter((function(t){return t.type===e})))},toDeleteCount:function(){return this.dismissedAnnotations.length-this.annotationsWithNewLabel.length},saveButtonClass:function(){return this.forceChange?"btn-danger":"btn-success"},sortingIsActive:function(){return this.isInDismissStep&&(this.sortingKey!==R||this.sortingDirection!==E)},imagesPinnable:function(){return this.needsSimilarityReference||this.sortingKey===P}},methods:{getAnnotations:function(t){var e,n,i=this;this.annotationsCache.hasOwnProperty(t.id)?e=Vue.Promise.resolve():(Vue.set(this.annotationsCache,t.id,[]),this.startLoading(),e=this.queryAnnotations(t).then((function(e){return i.gotAnnotations(t,e)}),l)),this.sortingKey===P?n=this.resetSorting():this.sortingIsActive?(this.sortingSequence=[],n=this.updateSortKey(this.sortingKey)):n=Vue.Promise.resolve(),Vue.Promise.all([e,n]).finally(this.finishLoading)},gotAnnotations:function(n,i){var s=i[0].data,o=i[1].data,a=[];s&&(a=a.concat(this.initAnnotations(n,s,t))),o&&(a=a.concat(this.initAnnotations(n,o,e))),a=a.sort((function(t,e){return e.id-t.id})),Vue.set(this.annotationsCache,n.id,a)},initAnnotations:function(t,e,n){return Object.keys(e).map((function(i){return{id:i,uuid:e[i],label_id:t.id,dismissed:!1,newLabel:null,type:n}}))},handleSelectedLabel:function(t){this.selectedLabel=t,this.isInDismissStep&&this.getAnnotations(t)},handleDeselectedLabel:function(){this.selectedLabel=null},handleSelectedImageDismiss:function(t,e){t.dismissed?(t.dismissed=!1,t.newLabel=null):(t.dismissed=!0,e.shiftKey&&this.lastSelectedImage?this.dismissAllImagesBetween(t,this.lastSelectedImage):this.lastSelectedImage=t)},goToRelabel:function(){this.step=1,this.lastSelectedImage=null},goToDismiss:function(){this.step=0,this.lastSelectedImage=null,this.selectedLabel&&this.getAnnotations(this.selectedLabel)},handleSelectedImageRelabel:function(t,e){t.newLabel?this.selectedLabel&&t.newLabel.id!==this.selectedLabel.id?t.newLabel=this.selectedLabel:t.newLabel=null:this.selectedLabel&&(t.newLabel=this.selectedLabel,e.shiftKey&&this.lastSelectedImage?this.relabelAllImagesBetween(t,this.lastSelectedImage):this.lastSelectedImage=t)},save:function(){var t=this;if(!this.loading){if(this.toDeleteCount>0){for(var e;null!==e&&parseInt(e,10)!==this.toDeleteCount;)e=prompt("This might delete ".concat(this.toDeleteCount," annotation(s). Please enter the number to continue."));if(null===e)return}this.startLoading(),this.performSave({dismissed_image_annotations:this.dismissedImageAnnotationsToSave,changed_image_annotations:this.changedImageAnnotationsToSave,dismissed_video_annotations:this.dismissedVideoAnnotationsToSave,changed_video_annotations:this.changedVideoAnnotationsToSave,force:this.forceChange}).then((function(e){return t.waitForSessionId=e.body.id}),(function(e){t.finishLoading(),l(e)}))}},handleSessionSaved:function(t){if(t.id==this.waitForSessionId){for(var e in this.finishLoading(),g.success("Saved. You can now start a new re-evaluation session."),this.step=0,this.annotationsCache)this.annotationsCache.hasOwnProperty(e)&&delete this.annotationsCache[e];for(var n in this.sortingSequenceCache)this.sortingSequenceCache.hasOwnProperty(n)&&delete this.sortingSequenceCache[n];this.handleSelectedLabel(this.selectedLabel)}},handleSessionFailed:function(t){t.id==this.waitForSessionId&&(this.finishLoading(),g.danger("There was an unexpected error."))},dismissAllImagesBetween:function(t,e){var n=this.sortedAnnotations.indexOf(t),i=this.sortedAnnotations.indexOf(e);if(i=0;n--)e.hasOwnProperty(t[n].label_id)?e[t[n].label_id].push(t[n].id):e[t[n].label_id]=[t[n].id];return e},packChangedToSave:function(t){for(var e={},n=t.length-1;n>=0;n--)e.hasOwnProperty(t[n].newLabel.id)?e[t[n].newLabel.id].push(t[n].id):e[t[n].newLabel.id]=[t[n].id];return e},initializeEcho:function(){a.getInstance().private("user-".concat(this.user.id)).listen(".Biigle\\Modules\\Largo\\Events\\LargoSessionSaved",this.handleSessionSaved).listen(".Biigle\\Modules\\Largo\\Events\\LargoSessionFailed",this.handleSessionFailed)},updateShowOutlines:function(t){this.showAnnotationOutlines=t},updateSortDirection:function(t){this.sortingDirection=t},fetchSortingSequence:function(t,e){var n,i,s,o=this,a=null===(n=this.sortingSequenceCache)||void 0===n||null===(i=n[e])||void 0===i?void 0:i[t];if(a)return Vue.Promise.resolve(a);if(this.selectedLabel)if(t===k)s=this.querySortByOutlier(e).then((function(t){return t.body}));else{if(t===P)return this.querySortBySimilarity(e,this.similarityReference).then((function(t){return t.body}));s=Vue.Promise.resolve([])}else s=Vue.Promise.resolve([]);return s.then((function(n){return o.putSortingSequenceToCache(t,e,n)}))},putSortingSequenceToCache:function(t,e,n){return this.sortingSequenceCache[e]||Vue.set(this.sortingSequenceCache,e,{}),this.sortingSequenceCache[e][t]=n,n},updateSortKey:function(t){var e,n=this;t!==P&&(this.similarityReference=null,this.pinnedImage=null);var i=null===(e=this.selectedLabel)||void 0===e?void 0:e.id;return this.startLoading(),this.fetchSortingSequence(t,i).then((function(e){n.sortingKey=t,n.sortingSequence=e,t===P&&(n.needsSimilarityReference=!1,n.pinnedImage=n.similarityReference)})).catch((function(t){n.handleErrorResponse(t),n.similarityReference=null})).finally(this.finishLoading)},handleInitSimilaritySort:function(){this.sortingKey!==P&&(this.needsSimilarityReference=!0)},handleCancelSimilaritySort:function(){this.needsSimilarityReference=!1},handlePinImage:function(t){var e;(null===(e=this.pinnedImage)||void 0===e?void 0:e.id)===t.id?this.resetSorting():this.imagesPinnable&&(this.similarityReference=t,this.updateSortKey(P))},resetSorting:function(){var t=this;return this.updateSortKey(R).then((function(){return t.sortingDirection=E}))}},watch:{annotations:function(t){r.$emit("annotations-count",t.length)},dismissedAnnotations:function(t){r.$emit("dismissed-annotations-count",t.length)},step:function(t){r.$emit("step",t)},selectedLabel:function(){this.isInDismissStep&&this.$refs.dismissGrid.setOffset(0)}},created:function(){var t=this;this.user=biigle.$require("largo.user"),window.addEventListener("beforeunload",(function(e){if(t.hasDismissedAnnotations)return e.preventDefault(),e.returnValue="","This page is asking you to confirm that you want to leave - data you have entered may not be saved."})),this.initializeEcho()}},undefined,undefined,!1,null,null,null).exports;const V=n({mixins:[D],components:{catalogImageGrid:_},data:function(){return{labelTrees:[]}},methods:{queryAnnotations:function(t){var e=C.queryImageAnnotations({id:t.id}),n=C.queryVideoAnnotations({id:t.id});return Vue.Promise.all([e,n])},showOutlines:function(){this.showAnnotationOutlines=!0},hideOutlines:function(){this.showAnnotationOutlines=!1}},created:function(){var t=biigle.$require("annotationCatalog.labelTree");this.labelTrees=[t],this.showAnnotationOutlines=!1}},undefined,undefined,!1,null,null,null).exports;const j=n({mixins:[D],data:function(){return{volumeId:null,labelTrees:[],mediaType:""}},methods:{queryAnnotations:function(t){var e,n;return"image"===this.mediaType?(e=o.queryImageAnnotations({id:this.volumeId,label_id:t.id}),n=Vue.Promise.resolve([])):(e=Vue.Promise.resolve([]),n=o.queryVideoAnnotations({id:this.volumeId,label_id:t.id})),Vue.Promise.all([e,n])},performSave:function(t){return o.save({id:this.volumeId},t)},querySortByOutlier:function(t){return o.sortAnnotationsByOutlier({id:this.volumeId,label_id:t}).then(this.parseSortingQuery)},querySortBySimilarity:function(t,e){return o.sortAnnotationsBySimilarity({id:this.volumeId,label_id:t,annotation_id:e.id}).then(this.parseSortingQuery)},parseSortingQuery:function(t){return"image"===this.mediaType?t.body=t.body.map((function(t){return"i"+t})):t.body=t.body.map((function(t){return"v"+t})),t}},created:function(){this.volumeId=biigle.$require("largo.volumeId"),this.labelTrees=biigle.$require("largo.labelTrees"),this.mediaType=biigle.$require("largo.mediaType")}},undefined,undefined,!1,null,null,null).exports;const G=n({data:function(){return{step:0,count:0,dismissedCount:0}},computed:{shownCount:function(){return this.isInDismissStep?this.count:this.dismissedCount},isInDismissStep:function(){return 0===this.step},isInRelabelStep:function(){return 1===this.step}},methods:{updateStep:function(t){this.step=t},updateCount:function(t){this.count=t},updateDismissedCount:function(t){this.dismissedCount=t}},created:function(){r.$on("annotations-count",this.updateCount),r.$on("dismissed-annotations-count",this.updateDismissedCount),r.$on("step",this.updateStep)}},undefined,undefined,!1,null,null,null).exports,K=Vue.resource("api/v1/projects{/id}/largo",{},{queryImageAnnotations:{method:"GET",url:"api/v1/projects{/id}/image-annotations/filter/label{/label_id}"},queryVideoAnnotations:{method:"GET",url:"api/v1/projects{/id}/video-annotations/filter/label{/label_id}"},sortAnnotationsByOutlier:{method:"GET",url:"api/v1/projects{/id}/annotations/sort/outliers{/label_id}"},sortAnnotationsBySimilarity:{method:"GET",url:"api/v1/projects{/id}/annotations/sort/similarity"}});const U=n({mixins:[D],data:function(){return{projectId:null,labelTrees:[]}},methods:{queryAnnotations:function(t){var e=K.queryImageAnnotations({id:this.projectId,label_id:t.id}),n=K.queryVideoAnnotations({id:this.projectId,label_id:t.id});return Vue.Promise.all([e,n])},performSave:function(t){return K.save({id:this.projectId},t)},querySortByOutlier:function(t){return K.sortAnnotationsByOutlier({id:this.projectId,label_id:t})},querySortBySimilarity:function(e,n){var i={id:this.projectId,label_id:e};return n.type===t?i.image_annotation_id=n.id:i.video_annotation_id=n.id,K.sortAnnotationsBySimilarity(i)}},created:function(){this.projectId=biigle.$require("largo.projectId"),this.labelTrees=biigle.$require("largo.labelTrees")}},undefined,undefined,!1,null,null,null).exports;biigle.$mount("annotation-catalog-container",V),biigle.$mount("largo-container",j),biigle.$mount("largo-title",G),biigle.$mount("project-largo-container",U)},307:()=>{}},n={};function i(t){var s=n[t];if(void 0!==s)return s.exports;var o=n[t]={exports:{}};return e[t](o,o.exports,i),o.exports}i.m=e,t=[],i.O=(e,n,s,o)=>{if(!n){var a=1/0;for(c=0;c=o)&&Object.keys(i.O).every((t=>i.O[t](n[l])))?n.splice(l--,1):(r=!1,o0&&t[c-1][2]>o;c--)t[c]=t[c-1];t[c]=[n,s,o]},i.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),(()=>{var t={355:0,392:0};i.O.j=e=>0===t[e];var e=(e,n)=>{var s,o,[a,r,l]=n,u=0;if(a.some((e=>0!==t[e]))){for(s in r)i.o(r,s)&&(i.m[s]=r[s]);if(l)var c=l(i)}for(e&&e(n);ui(543)));var s=i.O(void 0,[392],(()=>i(307)));s=i.O(s)})(); \ No newline at end of file +(()=>{"use strict";var t,e={529:()=>{var t="imageAnnotation",e="videoAnnotation";function n(t,e,n,i,s,o,a,r){var l,u="function"==typeof t?t.options:t;if(e&&(u.render=e,u.staticRenderFns=n,u._compiled=!0),i&&(u.functional=!0),o&&(u._scopeId="data-v-"+o),a?(l=function(t){(t=t||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext)||"undefined"==typeof __VUE_SSR_CONTEXT__||(t=__VUE_SSR_CONTEXT__),s&&s.call(this,t),t&&t._registeredComponents&&t._registeredComponents.add(a)},u._ssrRegister=l):s&&(l=r?function(){s.call(this,(u.functional?this.parent:this).$root.$options.shadowRoot)}:s),l)if(u.functional){u._injectStyles=l;var c=u.render;u.render=function(t,e){return l.call(e),c(t,e)}}else{var d=u.beforeCreate;u.beforeCreate=d?[].concat(d,l):[l]}return{exports:t,options:u}}const i=n({computed:{id:function(){return this.image.id},uuid:function(){return this.image.uuid},type:function(){return this.image.type},patchPrefix:function(){return this.uuid[0]+this.uuid[1]+"/"+this.uuid[2]+this.uuid[3]+"/"+this.uuid},urlTemplate:function(){return biigle.$require("largo.patchUrlTemplate")}},methods:{getThumbnailUrl:function(){return this.type===e?this.urlTemplate.replace(":prefix",this.patchPrefix).replace(":id","v-".concat(this.id)):this.urlTemplate.replace(":prefix",this.patchPrefix).replace(":id",this.id)}},created:function(){this.type===t?this.showAnnotationRoute=biigle.$require("largo.showImageAnnotationRoute"):this.showAnnotationRoute=biigle.$require("largo.showVideoAnnotationRoute")}},undefined,undefined,!1,null,null,null).exports;const s=n({mixins:[i],props:{_id:{type:String,required:!0},_uuid:{type:String,required:!0},label:{type:Object,required:!0},emptySrc:{type:String,required:!0},_urlTemplate:{type:String,required:!0}},data:function(){return{url:""}},computed:{title:function(){return"Example annotation for label "+this.label.name},src:function(){return this.url||this.emptySrc},image:function(){return{id:this._id,uuid:this._uuid,type:t}},urlTemplate:function(){return this._urlTemplate}},methods:{showEmptyImage:function(){this.url=""}},created:function(){this.url=this.getThumbnailUrl()}},undefined,undefined,!1,null,null,null).exports,o=Vue.resource("api/v1/volumes{/id}/largo",{},{queryImageAnnotations:{method:"GET",url:"api/v1/volumes{/id}/image-annotations/filter/label{/label_id}"},queryVideoAnnotations:{method:"GET",url:"api/v1/volumes{/id}/video-annotations/filter/label{/label_id}"},queryExampleAnnotations:{method:"GET",url:"api/v1/volumes{/id}/image-annotations/examples{/label_id}"},sortAnnotationsByOutlier:{method:"GET",url:"api/v1/volumes{/id}/annotations/sort/outliers{/label_id}"},sortAnnotationsBySimilarity:{method:"GET",url:"api/v1/volumes{/id}/annotations/sort/similarity"}});var a=biigle.$require("echo"),r=biigle.$require("events"),l=biigle.$require("messages").handleErrorResponse,u=biigle.$require("volumes.components.imageGrid"),c=biigle.$require("volumes.components.imageGridImage"),d=biigle.$require("annotations.components.labelsTabPlugins"),h=biigle.$require("labelTrees.components.labelTrees"),m=biigle.$require("core.mixins.loader"),f=biigle.$require("messages"),g=biigle.$require("core.components.powerToggle"),p=biigle.$require("core.models.Settings"),b=biigle.$require("annotations.components.settingsTabPlugins"),v=biigle.$require("core.components.sidebar"),y=biigle.$require("core.components.sidebarTab");const A=n({mixins:[m],components:{annotationPatch:s},props:{label:{default:null},volumeId:{type:Number,required:!0},count:{type:Number,default:3}},data:function(){return{exampleLabel:null,exampleAnnotations:[],cache:{},shown:!0}},computed:{isShown:function(){return this.shown&&null!==this.label},hasExamples:function(){return this.exampleLabel&&this.exampleAnnotations&&Object.keys(this.exampleAnnotations).length>0}},methods:{parseResponse:function(t){return t.data},setExampleAnnotations:function(t){(!t[0].hasOwnProperty("annotations")||Object.keys(t[0].annotations).length0){var n={};t.forEach((function(t){n[t.type===e?"v"+t.id:"i"+t.id]=t})),t=this.sortingSequence.map((function(t){return n[t]}))}return this.sortingDirection===E?t.slice().reverse():t},allAnnotations:function(){var t=[];for(var e in this.annotationsCache)this.annotationsCache.hasOwnProperty(e)&&(t=t.concat(this.annotationsCache[e]));return t},hasNoAnnotations:function(){return this.selectedLabel&&!this.loading&&0===this.annotations.length},dismissedAnnotations:function(){return this.allAnnotations.filter((function(t){return t.dismissed}))},annotationsWithNewLabel:function(){return this.dismissedAnnotations.filter((function(t){return!!t.newLabel}))},hasDismissedAnnotations:function(){return this.dismissedAnnotations.length>0},dismissedImageAnnotationsToSave:function(){return this.packDismissedToSave(this.dismissedAnnotations.filter((function(e){return e.type===t})))},dismissedVideoAnnotationsToSave:function(){return this.packDismissedToSave(this.dismissedAnnotations.filter((function(t){return t.type===e})))},changedImageAnnotationsToSave:function(){return this.packChangedToSave(this.annotationsWithNewLabel.filter((function(e){return e.type===t})))},changedVideoAnnotationsToSave:function(){return this.packChangedToSave(this.annotationsWithNewLabel.filter((function(t){return t.type===e})))},toDeleteCount:function(){return this.dismissedAnnotations.length-this.annotationsWithNewLabel.length},saveButtonClass:function(){return this.forceChange?"btn-danger":"btn-success"},sortingIsActive:function(){return this.isInDismissStep&&(this.sortingKey!==P||this.sortingDirection!==$)},imagesPinnable:function(){return this.needsSimilarityReference||this.sortingKey===k}},methods:{getAnnotations:function(t){var e,n,i=this;this.annotationsCache.hasOwnProperty(t.id)?e=Vue.Promise.resolve():(Vue.set(this.annotationsCache,t.id,[]),this.startLoading(),e=this.queryAnnotations(t).then((function(e){return i.gotAnnotations(t,e)}),l)),this.sortingKey===k?n=this.resetSorting():this.sortingIsActive?(this.sortingSequence=[],n=this.updateSortKey(this.sortingKey)):n=Vue.Promise.resolve(),Vue.Promise.all([e,n]).finally(this.finishLoading)},gotAnnotations:function(n,i){var s=i[0].data,o=i[1].data,a=[];s&&(a=a.concat(this.initAnnotations(n,s,t))),o&&(a=a.concat(this.initAnnotations(n,o,e))),a=a.sort((function(t,e){return e.id-t.id})),Vue.set(this.annotationsCache,n.id,a)},initAnnotations:function(t,e,n){return Object.keys(e).map((function(i){return{id:i,uuid:e[i],label_id:t.id,dismissed:!1,newLabel:null,type:n}}))},handleSelectedLabel:function(t){this.selectedLabel=t,this.isInDismissStep&&this.getAnnotations(t)},handleDeselectedLabel:function(){this.selectedLabel=null},handleSelectedImageDismiss:function(t,e){t.dismissed?(t.dismissed=!1,t.newLabel=null):(t.dismissed=!0,e.shiftKey&&this.lastSelectedImage?this.dismissAllImagesBetween(t,this.lastSelectedImage):this.lastSelectedImage=t)},goToRelabel:function(){this.step=1,this.lastSelectedImage=null},goToDismiss:function(){this.step=0,this.lastSelectedImage=null,this.selectedLabel&&this.getAnnotations(this.selectedLabel)},handleSelectedImageRelabel:function(t,e){t.newLabel?this.selectedLabel&&t.newLabel.id!==this.selectedLabel.id?t.newLabel=this.selectedLabel:t.newLabel=null:this.selectedLabel&&(t.newLabel=this.selectedLabel,e.shiftKey&&this.lastSelectedImage?this.relabelAllImagesBetween(t,this.lastSelectedImage):this.lastSelectedImage=t)},getChangedAnnotations:function(){var t=this,e={};return this.dismissedAnnotations.forEach((function(t){e[t.id]={oldLabelId:t.label_id,newLabelId:null}})),this.annotationsWithNewLabel.forEach((function(n){e[n.id]={oldLabelId:n.label_id,newLabelId:n.newLabel.id},t.annotationLabels.hasOwnProperty(n.newLabel.id)||(t.annotationLabels[n.newLabel.id]={id:n.newLabel.id,label:n.newLabel,count:0})})),e},save:function(){var t=this;if(!this.loading){if(this.toDeleteCount>0){for(var e;null!==e&&parseInt(e,10)!==this.toDeleteCount;)e=prompt("This might delete ".concat(this.toDeleteCount," annotation(s). Please enter the number to continue."));if(null===e)return}this.startLoading(),this.performSave({dismissed_image_annotations:this.dismissedImageAnnotationsToSave,changed_image_annotations:this.changedImageAnnotationsToSave,dismissed_video_annotations:this.dismissedVideoAnnotationsToSave,changed_video_annotations:this.changedVideoAnnotationsToSave,force:this.forceChange}).then((function(e){t.waitForSessionId=e.body.id,t.changedAnnotations=t.getChangedAnnotations()}),(function(e){t.finishLoading(),l(e)}))}},handleSessionSaved:function(t){if(t.id==this.waitForSessionId){for(var e in this.finishLoading(),f.success("Saved. You can now start a new re-evaluation session."),this.step=0,this.annotationsCache)this.annotationsCache.hasOwnProperty(e)&&delete this.annotationsCache[e];for(var n in this.sortingSequenceCache)this.sortingSequenceCache.hasOwnProperty(n)&&delete this.sortingSequenceCache[n];this.handleSelectedLabel(this.selectedLabel)}},handleSessionFailed:function(t){t.id==this.waitForSessionId&&(this.finishLoading(),f.danger("There was an unexpected error."))},dismissAllImagesBetween:function(t,e){var n=this.sortedAnnotations.indexOf(t),i=this.sortedAnnotations.indexOf(e);if(i=0;n--)e.hasOwnProperty(t[n].label_id)?e[t[n].label_id].push(t[n].id):e[t[n].label_id]=[t[n].id];return e},packChangedToSave:function(t){for(var e={},n=t.length-1;n>=0;n--)e.hasOwnProperty(t[n].newLabel.id)?e[t[n].newLabel.id].push(t[n].id):e[t[n].newLabel.id]=[t[n].id];return e},initializeEcho:function(){a.getInstance().private("user-".concat(this.user.id)).listen(".Biigle\\Modules\\Largo\\Events\\LargoSessionSaved",this.handleSessionSaved).listen(".Biigle\\Modules\\Largo\\Events\\LargoSessionFailed",this.handleSessionFailed)},updateShowOutlines:function(t){this.showAnnotationOutlines=t},updateSortDirection:function(t){this.sortingDirection=t},fetchSortingSequence:function(t,e){var n,i,s,o=this,a=null===(n=this.sortingSequenceCache)||void 0===n||null===(i=n[e])||void 0===i?void 0:i[t];if(a)return Vue.Promise.resolve(a);if(this.selectedLabel)if(t===R)s=this.querySortByOutlier(e).then((function(t){return t.body}));else{if(t===k)return this.querySortBySimilarity(e,this.similarityReference).then((function(t){return t.body}));s=Vue.Promise.resolve([])}else s=Vue.Promise.resolve([]);return s.then((function(n){return o.putSortingSequenceToCache(t,e,n)}))},putSortingSequenceToCache:function(t,e,n){return this.sortingSequenceCache[e]||Vue.set(this.sortingSequenceCache,e,{}),this.sortingSequenceCache[e][t]=n,n},updateSortKey:function(t){var e,n=this;t!==k&&(this.similarityReference=null,this.pinnedImage=null);var i=null===(e=this.selectedLabel)||void 0===e?void 0:e.id;return this.startLoading(),this.fetchSortingSequence(t,i).then((function(e){n.sortingKey=t,n.sortingSequence=e,t===k&&(n.needsSimilarityReference=!1,n.pinnedImage=n.similarityReference)})).catch((function(t){n.handleErrorResponse(t),n.similarityReference=null})).finally(this.finishLoading)},handleInitSimilaritySort:function(){this.sortingKey!==k&&(this.needsSimilarityReference=!0)},handleCancelSimilaritySort:function(){this.needsSimilarityReference=!1},handlePinImage:function(t){var e;(null===(e=this.pinnedImage)||void 0===e?void 0:e.id)===t.id?this.resetSorting():this.imagesPinnable&&(this.similarityReference=t,this.updateSortKey(k))},resetSorting:function(){var t=this;return this.updateSortKey(P).then((function(){return t.sortingDirection=$}))},handleOpenTab:function(t){"annotations"!==t||this.fetchedAllAnnotations||this.fetchAllAnnotations()},fetchAllAnnotations:function(){var t=this;this.startLoading(),Promise.all([I.fetchImageVolumeAnnotations({id:this.volumeId}),I.fetchVideoVolumeAnnotations({id:this.volumeId})]).then(this.parseResponse).then((function(e){t.addLabelsToAnnotationsTab(e[0]),t.addAnnotationsToCache(e[1])})).catch(l).finally(this.finishLoading)},parseResponse:function(n){var i=this,s=0!=n[0].body.length?n[0]:n[1],o=0!=n[0].body.length?t:e,a={},r=new Set,l={};return s.body.forEach((function(t){a=i.groupAnnotationsByLabel(t,o,a),r=i.createAnnotationTabItems(t,l,r)})),[l,a]},createAnnotationTabItems:function(t,e,n){var i=t.label_id,s=t.annotation_id+"-"+i;if(!n.has(s))if(e.hasOwnProperty(i))e[i].count+=1;else{n.add(s);var o=this.labelTreesIndex[t.label_tree_id].index,a=this.labelTreesIndex[t.label_tree_id].labelIndex[i],r=this.labelTrees[o].labels[a];e[i]={id:i,label:r,count:1}}return n},groupAnnotationsByLabel:function(t,e,n){var i=t.label_id,s={id:t.annotation_id,uuid:t.uuid,label_id:t.label_id,dismissed:!1,newLabel:null,type:e};return n.hasOwnProperty(i)?n[i].push(s):n[i]=[s],n},addAnnotationsToCache:function(t){var e=this;Object.keys(t).map((function(n){var i=t[n];i=i.sort((function(t,e){return e.id-t.id})),Vue.set(e.annotationsCache,n,i)})),this.fetchedAllAnnotations=!0},addLabelsToAnnotationsTab:function(t){this.annotationLabels=t}},watch:{annotations:function(t){r.$emit("annotations-count",t.length)},dismissedAnnotations:function(t){r.$emit("dismissed-annotations-count",t.length)},step:function(t){r.$emit("step",t)},selectedLabel:function(t,e){this.isInDismissStep&&this.$refs.dismissGrid.setOffset(0),null!=e&&e.selected&&(e.selected=!1)},labelTrees:function(){var t=this;this.labelTrees.forEach((function(e,n){var i={};e.labels.forEach((function(t,e){i[t.id]=e})),t.labelTreesIndex[e.id]={index:n,labelIndex:i}}))}},created:function(){var t=this;this.user=biigle.$require("largo.user"),this.volumeId=biigle.$require("largo.volumeId"),window.addEventListener("beforeunload",(function(e){if(t.hasDismissedAnnotations)return e.preventDefault(),e.returnValue="","This page is asking you to confirm that you want to leave - data you have entered may not be saved."})),this.initializeEcho()}},undefined,undefined,!1,null,null,null).exports;const G=n({mixins:[D],components:{catalogImageGrid:_},data:function(){return{labelTrees:[]}},methods:{queryAnnotations:function(t){var e=I.queryImageAnnotations({id:t.id}),n=I.queryVideoAnnotations({id:t.id});return Vue.Promise.all([e,n])},showOutlines:function(){this.showAnnotationOutlines=!0},hideOutlines:function(){this.showAnnotationOutlines=!1}},created:function(){var t=biigle.$require("annotationCatalog.labelTree");this.labelTrees=[t],this.showAnnotationOutlines=!1}},undefined,undefined,!1,null,null,null).exports;const K=n({mixins:[D],data:function(){return{volumeId:null,labelTrees:[],mediaType:""}},methods:{queryAnnotations:function(t){var e,n;return"image"===this.mediaType?(e=o.queryImageAnnotations({id:this.volumeId,label_id:t.id}),n=Vue.Promise.resolve([])):(e=Vue.Promise.resolve([]),n=o.queryVideoAnnotations({id:this.volumeId,label_id:t.id})),Vue.Promise.all([e,n])},performSave:function(t){return o.save({id:this.volumeId},t)},querySortByOutlier:function(t){return o.sortAnnotationsByOutlier({id:this.volumeId,label_id:t}).then(this.parseSortingQuery)},querySortBySimilarity:function(t,e){return o.sortAnnotationsBySimilarity({id:this.volumeId,label_id:t,annotation_id:e.id}).then(this.parseSortingQuery)},parseSortingQuery:function(t){return"image"===this.mediaType?t.body=t.body.map((function(t){return"i"+t})):t.body=t.body.map((function(t){return"v"+t})),t}},created:function(){this.volumeId=biigle.$require("largo.volumeId"),this.labelTrees=biigle.$require("largo.labelTrees"),this.mediaType=biigle.$require("largo.mediaType")}},undefined,undefined,!1,null,null,null).exports;const U=n({data:function(){return{step:0,count:0,dismissedCount:0}},computed:{shownCount:function(){return this.isInDismissStep?this.count:this.dismissedCount},isInDismissStep:function(){return 0===this.step},isInRelabelStep:function(){return 1===this.step}},methods:{updateStep:function(t){this.step=t},updateCount:function(t){this.count=t},updateDismissedCount:function(t){this.dismissedCount=t}},created:function(){r.$on("annotations-count",this.updateCount),r.$on("dismissed-annotations-count",this.updateDismissedCount),r.$on("step",this.updateStep)}},undefined,undefined,!1,null,null,null).exports,N=Vue.resource("api/v1/projects{/id}/largo",{},{queryImageAnnotations:{method:"GET",url:"api/v1/projects{/id}/image-annotations/filter/label{/label_id}"},queryVideoAnnotations:{method:"GET",url:"api/v1/projects{/id}/video-annotations/filter/label{/label_id}"},sortAnnotationsByOutlier:{method:"GET",url:"api/v1/projects{/id}/annotations/sort/outliers{/label_id}"},sortAnnotationsBySimilarity:{method:"GET",url:"api/v1/projects{/id}/annotations/sort/similarity"},getAllProjectsImageAnnotationLabels:{method:"GET",url:"api/v1/projects{/id}/image-annotations"},getAllProjectsVideoAnnotationLabels:{method:"GET",url:"api/v1/projects{/id}/video-annotations"}}),F={mixins:[D],data:function(){return{projectId:null,labelTrees:[],annotationLabels:{}}},methods:{queryAnnotations:function(t){var e=N.queryImageAnnotations({id:this.projectId,label_id:t.id}),n=N.queryVideoAnnotations({id:this.projectId,label_id:t.id});return Vue.Promise.all([e,n])},performSave:function(t){return N.save({id:this.projectId},t)},querySortByOutlier:function(t){return N.sortAnnotationsByOutlier({id:this.projectId,label_id:t})},querySortBySimilarity:function(e,n){var i={id:this.projectId,label_id:e};return n.type===t?i.image_annotation_id=n.id:i.video_annotation_id=n.id,N.sortAnnotationsBySimilarity(i)},fetchAllAnnotations:function(){var t=this,e={body:[]};this.startLoading(),Promise.all([N.getAllProjectsImageAnnotationLabels({id:this.projectId}).then((function(n){return t.parseResponse([n,e])})),N.getAllProjectsVideoAnnotationLabels({id:this.projectId}).then((function(n){return t.parseResponse([e,n])}))]).then((function(e){t.addLabelsToAnnotationsTab([e[0][0],e[1][0]]),t.addAnnotationsToCache([e[0][1],e[1][1]])})).catch(l).finally(this.finishLoading)},addAnnotationsToCache:function(t){var e=this;new Set(t.map((function(t){return Object.keys(t)})).flat()).forEach((function(n){var i=[];i=(i=t[0].hasOwnProperty(n)&&t[1].hasOwnProperty(n)?t[0][n].concat(t[1][n]):t[0].hasOwnProperty(n)?t[0][n]:t[1][n]).sort((function(t,e){return e.id-t.id})),Vue.set(e.annotationsCache,n,i)})),this.fetchedAllAnnotations=!0},addLabelsToAnnotationsTab:function(t){var e=new Set(t.map((function(t){return Object.keys(t)})).flat()),n={};e.forEach((function(e){t[0].hasOwnProperty(e)&&t[1].hasOwnProperty(e)?(n[e]=t[1][e],n[e].count=t[0][e].count+t[1][e].count):t[0].hasOwnProperty(e)?n[e]=t[0][e]:n[e]=t[1][e]})),this.annotationLabels=n}},created:function(){this.projectId=biigle.$require("largo.projectId"),this.labelTrees=biigle.$require("largo.labelTrees")}};const H=n(F,undefined,undefined,!1,null,null,null).exports;biigle.$mount("annotation-catalog-container",G),biigle.$mount("largo-container",K),biigle.$mount("largo-title",U),biigle.$mount("project-largo-container",H)},307:()=>{}},n={};function i(t){var s=n[t];if(void 0!==s)return s.exports;var o=n[t]={exports:{}};return e[t](o,o.exports,i),o.exports}i.m=e,t=[],i.O=(e,n,s,o)=>{if(!n){var a=1/0;for(c=0;c=o)&&Object.keys(i.O).every((t=>i.O[t](n[l])))?n.splice(l--,1):(r=!1,o0&&t[c-1][2]>o;c--)t[c]=t[c-1];t[c]=[n,s,o]},i.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),(()=>{var t={355:0,392:0};i.O.j=e=>0===t[e];var e=(e,n)=>{var s,o,[a,r,l]=n,u=0;if(a.some((e=>0!==t[e]))){for(s in r)i.o(r,s)&&(i.m[s]=r[s]);if(l)var c=l(i)}for(e&&e(n);ui(529)));var s=i.O(void 0,[392],(()=>i(307)));s=i.O(s)})(); \ No newline at end of file diff --git a/src/public/mix-manifest.json b/src/public/mix-manifest.json index 0d8219d..6828606 100644 --- a/src/public/mix-manifest.json +++ b/src/public/mix-manifest.json @@ -1,4 +1,4 @@ { - "/assets/scripts/main.js": "/assets/scripts/main.js?id=ed8d9b904adadcdce546b7b39d73ef4c", + "/assets/scripts/main.js": "/assets/scripts/main.js?id=c9d6f3c26bf3c9e235d9da85fcf25260", "/assets/styles/main.css": "/assets/styles/main.css?id=68a88f330c08af97df1d47c207ea8ccb" } diff --git a/src/resources/assets/js/api/labels.js b/src/resources/assets/js/api/labels.js index 0afbe70..789448b 100644 --- a/src/resources/assets/js/api/labels.js +++ b/src/resources/assets/js/api/labels.js @@ -20,4 +20,12 @@ export default Vue.resource('api/v1/labels{/id}', {}, { method: 'GET', url: 'api/v1/labels{/id}/video-annotations', }, + fetchImageVolumeAnnotations: { + method: 'GET', + url: 'api/v1/volume{/id}/image-annotations', + }, + fetchVideoVolumeAnnotations: { + method: 'GET', + url: 'api/v1/volume{/id}/video-annotations', + } }); diff --git a/src/resources/assets/js/api/projects.js b/src/resources/assets/js/api/projects.js index 2c9d31d..d09a5cc 100644 --- a/src/resources/assets/js/api/projects.js +++ b/src/resources/assets/js/api/projects.js @@ -31,4 +31,12 @@ export default Vue.resource('api/v1/projects{/id}/largo', {}, { method: 'GET', url: 'api/v1/projects{/id}/annotations/sort/similarity', }, + getAllProjectsImageAnnotationLabels: { + method: 'GET', + url: 'api/v1/projects{/id}/image-annotations' + }, + getAllProjectsVideoAnnotationLabels: { + method: 'GET', + url: 'api/v1/projects{/id}/video-annotations' + } }); diff --git a/src/resources/assets/js/components/annotationTabLabelItem.vue b/src/resources/assets/js/components/annotationTabLabelItem.vue new file mode 100644 index 0000000..a691be9 --- /dev/null +++ b/src/resources/assets/js/components/annotationTabLabelItem.vue @@ -0,0 +1,63 @@ + + + diff --git a/src/resources/assets/js/components/annotationsTab.vue b/src/resources/assets/js/components/annotationsTab.vue new file mode 100644 index 0000000..915c166 --- /dev/null +++ b/src/resources/assets/js/components/annotationsTab.vue @@ -0,0 +1,63 @@ + diff --git a/src/resources/assets/js/mixins/largoContainer.vue b/src/resources/assets/js/mixins/largoContainer.vue index f8c24a9..f89f4f4 100644 --- a/src/resources/assets/js/mixins/largoContainer.vue +++ b/src/resources/assets/js/mixins/largoContainer.vue @@ -13,7 +13,9 @@ import {Messages} from '../import'; import {PowerToggle} from '../import'; import {SidebarTab} from '../import'; import {Sidebar} from '../import'; +import AnnotationsTab from '../components/annotationsTab.vue'; import {SORT_DIRECTION, SORT_KEY} from '../components/sortingTab'; +import AnnotationsApi from '../api/labels'; /** * Mixin for largo view models @@ -29,6 +31,7 @@ export default { relabelImageGrid: RelabelImageGrid, settingsTab: SettingsTab, sortingTab: SortingTab, + annotationsTab: AnnotationsTab, }, data() { return { @@ -51,6 +54,11 @@ export default { needsSimilarityReference: false, similarityReference: null, pinnedImage: null, + annotationLabels: {}, + volumeId: 0, + fetchedAllAnnotations: false, + changedAnnotations: {}, + labelTreesIndex: {}, }; }, provide() { @@ -204,6 +212,7 @@ export default { annotations = annotations.sort((a, b) => b.id - a.id); Vue.set(this.annotationsCache, label.id, annotations); + }, initAnnotations(label, annotations, type) { return Object.keys(annotations) @@ -269,6 +278,27 @@ export default { } } }, + getChangedAnnotations() { + let changedAnnotations = {}; + + this.dismissedAnnotations.forEach((a) => { + changedAnnotations[a.id] = { oldLabelId: a.label_id, newLabelId: null }; + }) + this.annotationsWithNewLabel.forEach((a) => { + changedAnnotations[a.id] = { oldLabelId: a.label_id, newLabelId: a.newLabel.id }; + // Add new label to annotation tab + if (!this.annotationLabels.hasOwnProperty(a.newLabel.id)) { + this.annotationLabels[a.newLabel.id] = { + id: a.newLabel.id, + label: a.newLabel, + count: 0 + } + } + + }) + + return changedAnnotations; + }, save() { if (this.loading) { return @@ -294,7 +324,10 @@ export default { force: this.forceChange, }) .then( - response => this.waitForSessionId = response.body.id, + (response) => { + this.waitForSessionId = response.body.id; + this.changedAnnotations = this.getChangedAnnotations(); + }, (response) => { this.finishLoading(); handleErrorResponse(response); @@ -469,6 +502,92 @@ export default { return this.updateSortKey(SORT_KEY.ANNOTATION_ID) .then(() => this.sortingDirection = SORT_DIRECTION.DESCENDING); }, + handleOpenTab(tab) { + if (tab === "annotations" && !this.fetchedAllAnnotations) { + this.fetchAllAnnotations(); + } + }, + fetchAllAnnotations() { + this.startLoading(); + Promise.all([ + AnnotationsApi.fetchImageVolumeAnnotations({ id: this.volumeId }), + AnnotationsApi.fetchVideoVolumeAnnotations({ id: this.volumeId }) + ]) + .then(this.parseResponse) + .then((res) => { + this.addLabelsToAnnotationsTab(res[0]); + this.addAnnotationsToCache(res[1]); + }) + .catch(handleErrorResponse) + .finally(this.finishLoading); + }, + parseResponse(responses) { + let res = responses[0].body.length != 0 ? responses[0] : responses[1]; + let type = responses[0].body.length != 0 ? IMAGE_ANNOTATION : VIDEO_ANNOTATION; + let groupedAnnotations = {}; + let uniqueKeys = new Set(); + let labels = {}; + res.body.forEach((al) => { + // Save annotations to use them in labels tab + groupedAnnotations = this.groupAnnotationsByLabel(al, type, groupedAnnotations); + labels, uniqueKeys = this.createAnnotationTabItems(al, labels, uniqueKeys); + }) + // Save all video and image annotation labels for project largo view + return [labels, groupedAnnotations]; + }, + createAnnotationTabItems(al, labels, uniqueKeys) { + let labelId = al.label_id; + // Make sure each annotation is added only once for each label item. + // This is important if the annotation has the same label attached by + // multiple users. + let uniqueKey = al.annotation_id + '-' + labelId; + if (!uniqueKeys.has(uniqueKey)) { + if (labels.hasOwnProperty(labelId)) { + labels[labelId].count += 1; + } else { + uniqueKeys.add(uniqueKey); + let tIdx = this.labelTreesIndex[al.label_tree_id].index; + let lIdx = this.labelTreesIndex[al.label_tree_id].labelIndex[labelId]; + let label = this.labelTrees[tIdx].labels[lIdx]; + labels[labelId] = { + id: labelId, + label: label, + count: 1, + }; + } + } + return labels, uniqueKeys; + }, + groupAnnotationsByLabel(al, type, groupedAnnotation) { + let labelId = al.label_id; + let annotation = { + id: al.annotation_id, + uuid: al.uuid, + label_id: al.label_id, + dismissed: false, + newLabel: null, + type: type + }; + if (groupedAnnotation.hasOwnProperty(labelId)) { + groupedAnnotation[labelId].push(annotation); + } else { + groupedAnnotation[labelId] = [annotation]; + } + return groupedAnnotation; + }, + addAnnotationsToCache(groupedAnnotation) { + Object.keys(groupedAnnotation).map((id) => { + // Show the newest annotations (with highest ID) first. + let annotations = groupedAnnotation[id]; + annotations = annotations.sort((a, b) => b.id - a.id); + Vue.set(this.annotationsCache, id, annotations); + }); + + this.fetchedAllAnnotations = true; + }, + addLabelsToAnnotationsTab(labels) { + this.annotationLabels = labels; + } }, watch: { annotations(annotations) { @@ -480,14 +599,27 @@ export default { step(step) { Events.$emit('step', step); }, - selectedLabel() { + selectedLabel(_, oldLabel) { if (this.isInDismissStep) { this.$refs.dismissGrid.setOffset(0); } + if (oldLabel?.selected) { + oldLabel.selected = false; + } }, + labelTrees() { + this.labelTrees.forEach((t, idx) => { + let labelIndex = {}; + t.labels.forEach((l, idx) => { + labelIndex[l.id] = idx; + }); + this.labelTreesIndex[t.id] = { index: idx, labelIndex: labelIndex }; + }) + } }, created() { this.user = biigle.$require('largo.user'); + this.volumeId = biigle.$require('largo.volumeId'); window.addEventListener('beforeunload', (e) => { if (this.hasDismissedAnnotations) { diff --git a/src/resources/assets/js/projectLargoContainer.vue b/src/resources/assets/js/projectLargoContainer.vue index c3c03ec..278b90b 100644 --- a/src/resources/assets/js/projectLargoContainer.vue +++ b/src/resources/assets/js/projectLargoContainer.vue @@ -2,6 +2,7 @@ import LargoContainer from './mixins/largoContainer'; import ProjectsApi from './api/projects'; import {IMAGE_ANNOTATION} from './constants'; +import {handleErrorResponse} from './import'; /** * View model for the main Largo container (for projects) @@ -12,6 +13,7 @@ export default { return { projectId: null, labelTrees: [], + annotationLabels: {} }; }, methods: { @@ -41,6 +43,56 @@ export default { return ProjectsApi.sortAnnotationsBySimilarity(params); }, + fetchAllAnnotations() { + let emptyResponse = { body: [] }; + this.startLoading(); + Promise.all([ + ProjectsApi.getAllProjectsImageAnnotationLabels({ id: this.projectId }) + .then((res) => { return this.parseResponse([res, emptyResponse]) }), + ProjectsApi.getAllProjectsVideoAnnotationLabels({ id: this.projectId }) + .then((res) => { return this.parseResponse([emptyResponse, res]) }), + ]) + .then((responses) => { + this.addLabelsToAnnotationsTab([responses[0][0], responses[1][0]]) + this.addAnnotationsToCache([responses[0][1], responses[1][1]]) + }) + .catch(handleErrorResponse) + .finally(this.finishLoading); + }, + addAnnotationsToCache(responses) { + let lids = new Set(responses.map((res) => Object.keys(res)).flat()); + lids.forEach(id => { + let annotations = []; + if (responses[0].hasOwnProperty(id) && responses[1].hasOwnProperty(id)) { + annotations = responses[0][id].concat(responses[1][id]); + } else if (responses[0].hasOwnProperty(id)) { + annotations = responses[0][id]; + } else { + annotations = responses[1][id]; + } + // Show the newest annotations (with highest ID) first. + annotations = annotations.sort((a, b) => b.id - a.id); + Vue.set(this.annotationsCache, id, annotations); + }) + + this.fetchedAllAnnotations = true; + }, + addLabelsToAnnotationsTab(responses){ + let lids = new Set(responses.map((res) => Object.keys(res)).flat()); + let labels = {}; + lids.forEach(id => { + if (responses[0].hasOwnProperty(id) && responses[1].hasOwnProperty(id)) { + labels[id] = responses[1][id]; + labels[id].count = responses[0][id].count + responses[1][id].count; + + } else if (responses[0].hasOwnProperty(id)) { + labels[id] = responses[0][id]; + } else { + labels[id] = responses[1][id]; + } + }); + this.annotationLabels = labels; + } }, created() { this.projectId = biigle.$require('largo.projectId'); diff --git a/src/resources/views/annotationsTab.php b/src/resources/views/annotationsTab.php new file mode 100644 index 0000000..1debfb2 --- /dev/null +++ b/src/resources/views/annotationsTab.php @@ -0,0 +1,30 @@ + + +
+
+
Total + +
+
+
    + +
+
+
+
diff --git a/src/resources/views/show/content.blade.php b/src/resources/views/show/content.blade.php index 1ddbe3d..eb622fe 100644 --- a/src/resources/views/show/content.blade.php +++ b/src/resources/views/show/content.blade.php @@ -27,7 +27,7 @@
-
+
Please choose a label in the sidebar.
@@ -35,7 +35,8 @@
- + + @include('largo::annotationsTab')
diff --git a/tests/Http/Controllers/Api/Projects/FilterImageAnnotationsByLabelControllerTest.php b/tests/Http/Controllers/Api/Projects/FilterImageAnnotationsByLabelControllerTest.php index 6972d9e..0bfa640 100644 --- a/tests/Http/Controllers/Api/Projects/FilterImageAnnotationsByLabelControllerTest.php +++ b/tests/Http/Controllers/Api/Projects/FilterImageAnnotationsByLabelControllerTest.php @@ -3,9 +3,14 @@ namespace Biigle\Tests\Modules\Largo\Http\Controllers\Api\Projects; use ApiTestCase; -use Biigle\Tests\ImageAnnotationLabelTest; -use Biigle\Tests\ImageAnnotationTest; use Biigle\Tests\ImageTest; +use Biigle\Tests\LabelTest; +use Biigle\Tests\VolumeTest; +use Biigle\Tests\ProjectTest; +use Illuminate\Testing\TestResponse; +use Biigle\Tests\ImageAnnotationTest; +use Biigle\Tests\ImageAnnotationLabelTest; +use Symfony\Component\HttpFoundation\Response; class FilterImageAnnotationsByLabelControllerTest extends ApiTestCase { @@ -74,4 +79,117 @@ public function testIndexDuplicate() ->assertStatus(200) ->assertExactJson([$a1->id => $image->uuid]); } + + public function testGetProjectsAnnotationLabels() + { + $v1 = VolumeTest::create(); + $img1 = ImageTest::create(['volume_id' => $v1->id]); + $a1 = ImageAnnotationTest::create(['image_id' => $img1]); + $l1 = LabelTest::create(); + ImageAnnotationLabelTest::create(['annotation_id' => $a1->id, 'label_id' => $l1->id]); + + + $v2 = VolumeTest::create(); + $img2 = ImageTest::create(['volume_id' => $v2->id]); + $a2 = ImageAnnotationTest::create(['image_id' => $img2->id]); + $l2 = LabelTest::create(); + ImageAnnotationLabelTest::create(['annotation_id' => $a2->id, 'label_id' => $l2->id]); + + $this->project()->volumes()->attach([$v1->id, $v2->id]); + $pid = $this->project()->id; + + $this->doTestApiRoute('GET', "/api/v1/projects/{$pid}/image-annotations/"); + + $this->beUser(); + $this->getJson("/api/v1/projects/{$pid}/image-annotations/") + ->assertStatus(403); + + $this->beEditor(); + $response = $this->getJson("/api/v1/projects/{$pid}/image-annotations/")->assertStatus(200); + + ob_start(); + $response->sendContent(); + $content = ob_get_clean(); + $response = new TestResponse( + new Response( + $content, + $response->baseResponse->getStatusCode(), + $response->baseResponse->headers->all() + ) + ); + + $response->assertJsonFragment([ + 'uuid' => $img1->uuid, + 'annotation_id' => $a1->id, + 'label_id' => $l1->id, + 'label_tree_id' => $l1->label_tree_id + ]) + ->assertJsonFragment([ + 'uuid' => $img2->uuid, + 'annotation_id' => $a2->id, + 'label_id' => $l2->id, + 'label_tree_id' => $l2->label_tree_id + ]); + + $this->assertCount(2, json_decode($response->getContent())); + } + + public function testGetEmptyProjectsAnnotationLabels() + { + $pid = $this->project()->id; + + $this->doTestApiRoute('GET', "/api/v1/projects/{$pid}/image-annotations/"); + + $this->beUser(); + $this->getJson("/api/v1/projects/{$pid}/image-annotations/") + ->assertStatus(403); + + $this->beEditor(); + $response = $this->getJson("/api/v1/projects/{$pid}/image-annotations/")->assertStatus(200); + + ob_start(); + $response->sendContent(); + $content = ob_get_clean(); + $response = new TestResponse( + new Response( + $content, + $response->baseResponse->getStatusCode(), + $response->baseResponse->headers->all() + ) + ); + + $this->assertEmpty(json_decode($response->getContent())); + } + + public function testGetProjectsWithoutAnnotationsAnnotationLabels() + { + $project = ProjectTest::create(); + $v1 = VolumeTest::create(); + ImageTest::create(['volume_id' => $v1->id]); + + $project->volumes()->attach($v1->id); + $pid = $this->project()->id; + + $this->doTestApiRoute('GET', "/api/v1/projects/{$pid}/image-annotations/"); + + $this->beUser(); + $this->getJson("/api/v1/projects/{$pid}/image-annotations/") + ->assertStatus(403); + + $this->beEditor(); + $response = $this->getJson("/api/v1/projects/{$pid}/image-annotations/")->assertStatus(200); + + ob_start(); + $response->sendContent(); + $content = ob_get_clean(); + $response = new TestResponse( + new Response( + $content, + $response->baseResponse->getStatusCode(), + $response->baseResponse->headers->all() + ) + ); + + $this->assertEmpty(json_decode($response->getContent())); + } } diff --git a/tests/Http/Controllers/Api/Projects/FilterVideoAnnotationsByLabelControllerTest.php b/tests/Http/Controllers/Api/Projects/FilterVideoAnnotationsByLabelControllerTest.php index a11d215..10a1c31 100644 --- a/tests/Http/Controllers/Api/Projects/FilterVideoAnnotationsByLabelControllerTest.php +++ b/tests/Http/Controllers/Api/Projects/FilterVideoAnnotationsByLabelControllerTest.php @@ -3,9 +3,16 @@ namespace Biigle\Tests\Modules\Largo\Http\Controllers\Api\Projects; use ApiTestCase; -use Biigle\Tests\VideoAnnotationLabelTest; -use Biigle\Tests\VideoAnnotationTest; +use Carbon\Carbon; +use Biigle\MediaType; +use Biigle\Tests\LabelTest; use Biigle\Tests\VideoTest; +use Biigle\Tests\VolumeTest; +use Biigle\Tests\ProjectTest; +use Illuminate\Testing\TestResponse; +use Biigle\Tests\VideoAnnotationTest; +use Biigle\Tests\VideoAnnotationLabelTest; +use Symfony\Component\HttpFoundation\Response; class FilterVideoAnnotationsByLabelControllerTest extends ApiTestCase { @@ -74,4 +81,117 @@ public function testIndexDuplicate() ->assertStatus(200) ->assertExactJson([$a1->id => $video->uuid]); } + + public function testGetProjectsAnnotationLabels() + { + $v1 = VolumeTest::create(['media_type_id' => MediaType::videoId()]); + $vid1 = VideoTest::create(['volume_id' => $v1->id]); + $a1 = VideoAnnotationTest::create(['video_id' => $vid1]); + $l1 = LabelTest::create(); + VideoAnnotationLabelTest::create(['annotation_id' => $a1->id, 'label_id' => $l1->id]); + + + $v2 = VolumeTest::create(['media_type_id' => MediaType::videoId()]); + $vid2 = VideoTest::create(['volume_id' => $v2->id]); + $a2 = VideoAnnotationTest::create(['video_id' => $vid2->id]); + $l2 = LabelTest::create(); + VideoAnnotationLabelTest::create(['annotation_id' => $a2->id, 'label_id' => $l2->id]); + + $this->project()->volumes()->attach([$v1->id, $v2->id]); + $pid = $this->project()->id; + + $this->doTestApiRoute('GET', "/api/v1/projects/{$pid}/video-annotations/"); + + $this->beUser(); + $this->getJson("/api/v1/projects/{$pid}/video-annotations/") + ->assertStatus(403); + + $this->beEditor(); + $response = $this->getJson("/api/v1/projects/{$pid}/video-annotations/")->assertStatus(200); + + ob_start(); + $response->sendContent(); + $content = ob_get_clean(); + $response = new TestResponse( + new Response( + $content, + $response->baseResponse->getStatusCode(), + $response->baseResponse->headers->all() + ) + ); + + $response->assertJsonFragment([ + 'uuid' => $vid1->uuid, + 'annotation_id' => $a1->id, + 'label_id' => $l1->id, + 'label_tree_id' => $l1->label_tree_id + ]) + ->assertJsonFragment([ + 'uuid' => $vid2->uuid, + 'annotation_id' => $a2->id, + 'label_id' => $l2->id, + 'label_tree_id' => $l2->label_tree_id + ]); + + $this->assertCount(2, json_decode($response->getContent())); + } + + public function testGetEmptyProjectsAnnotationLabels() + { + $pid = $this->project()->id; + + $this->doTestApiRoute('GET', "/api/v1/projects/{$pid}/image-annotations/"); + + $this->beUser(); + $this->getJson("/api/v1/projects/{$pid}/image-annotations/") + ->assertStatus(403); + + $this->beEditor(); + $response = $this->getJson("/api/v1/projects/{$pid}/image-annotations/")->assertStatus(200); + + ob_start(); + $response->sendContent(); + $content = ob_get_clean(); + $response = new TestResponse( + new Response( + $content, + $response->baseResponse->getStatusCode(), + $response->baseResponse->headers->all() + ) + ); + + $this->assertEmpty(json_decode($response->getContent())); + } + + public function testGetProjectWithoutAnnotationsAnnotationLabels() + { + $project = ProjectTest::create(); + $v1 = VolumeTest::create(['media_type_id' => MediaType::videoId()]); + VideoTest::create(['volume_id' => $v1->id]); + + $project->volumes()->attach($v1->id); + $pid = $this->project()->id; + + $this->doTestApiRoute('GET', "/api/v1/projects/{$pid}/image-annotations/"); + + $this->beUser(); + $this->getJson("/api/v1/projects/{$pid}/image-annotations/") + ->assertStatus(403); + + $this->beEditor(); + $response = $this->getJson("/api/v1/projects/{$pid}/image-annotations/")->assertStatus(200); + + ob_start(); + $response->sendContent(); + $content = ob_get_clean(); + $response = new TestResponse( + new Response( + $content, + $response->baseResponse->getStatusCode(), + $response->baseResponse->headers->all() + ) + ); + + $this->assertEmpty(json_decode($response->getContent())); + } } diff --git a/tests/Http/Controllers/Api/Volumes/FilterImageAnnotationsByLabelControllerTest.php b/tests/Http/Controllers/Api/Volumes/FilterImageAnnotationsByLabelControllerTest.php index ef3ec0b..3534d95 100644 --- a/tests/Http/Controllers/Api/Volumes/FilterImageAnnotationsByLabelControllerTest.php +++ b/tests/Http/Controllers/Api/Volumes/FilterImageAnnotationsByLabelControllerTest.php @@ -3,11 +3,15 @@ namespace Biigle\Tests\Modules\Largo\Http\Controllers\Api\Volumes; use ApiTestCase; +use Carbon\Carbon; +use Biigle\Tests\ImageTest; +use Biigle\Tests\LabelTest; +use Biigle\Tests\ProjectTest; +use Illuminate\Testing\TestResponse; +use Biigle\Tests\ImageAnnotationTest; use Biigle\Tests\AnnotationSessionTest; use Biigle\Tests\ImageAnnotationLabelTest; -use Biigle\Tests\ImageAnnotationTest; -use Biigle\Tests\ImageTest; -use Carbon\Carbon; +use Symfony\Component\HttpFoundation\Response; class FilterImageAnnotationsByLabelControllerTest extends ApiTestCase { @@ -188,4 +192,85 @@ public function testIndexDuplicate() ->assertStatus(200) ->assertExactJson([$a1->id => $image->uuid]); } + + public function testGetVolumeAnnotationLabels() + { + $id = $this->volume()->id; + $project = ProjectTest::create(); + $img1 = ImageTest::create(['volume_id' => $id, 'filename' => 'abc.jpg']); + $img2 = ImageTest::create(['volume_id' => $id, 'filename' => 'def.jpg']); + $a1 = ImageAnnotationTest::create(['image_id' => $img1]); + $a2 = ImageAnnotationTest::create(['image_id' => $img2->id]); + $l1 = LabelTest::create(); + $l2 = LabelTest::create(); + ImageAnnotationLabelTest::create(['annotation_id' => $a1->id, 'label_id' => $l1->id]); + ImageAnnotationLabelTest::create(['annotation_id' => $a2->id, 'label_id' => $l2->id]); + + $project->volumes()->attach($id); + + $this->doTestApiRoute('GET', "/api/v1/volume/{$id}/image-annotations"); + + $this->beUser(); + $this->getJson("/api/v1/volume/{$id}/image-annotations") + ->assertStatus(403); + + $this->beEditor(); + $response = $this->getJson("/api/v1/volume/{$id}/image-annotations")->assertStatus(200); + + ob_start(); + $response->sendContent(); + $content = ob_get_clean(); + $response = new TestResponse( + new Response( + $content, + $response->baseResponse->getStatusCode(), + $response->baseResponse->headers->all() + ) + ); + + $response->assertJsonFragment([ + 'uuid' => $img1->uuid, + 'annotation_id' => $a1->id, + 'label_id' => $l1->id, + 'label_tree_id' => $l1->label_tree_id + ]) + ->assertJsonFragment([ + 'uuid' => $img2->uuid, + 'annotation_id' => $a2->id, + 'label_id' => $l2->id, + 'label_tree_id' => $l2->label_tree_id + ]); + + $this->assertCount(2, json_decode($response->getContent())); + } + + public function testGetVolumeWithoutAnnotationsAnnotationLabels() + { + $id = $this->volume()->id; + $project = ProjectTest::create(); + ImageTest::create(['volume_id' => $this->volume()->id]); + $project->volumes()->attach($id); + + $this->doTestApiRoute('GET', "/api/v1/volume/{$id}/image-annotations"); + + $this->beUser(); + $this->getJson("/api/v1/volume/{$id}/image-annotations") + ->assertStatus(403); + + $this->beEditor(); + $response = $this->getJson("/api/v1/volume/{$id}/image-annotations")->assertStatus(200); + + ob_start(); + $response->sendContent(); + $content = ob_get_clean(); + $response = new TestResponse( + new Response( + $content, + $response->baseResponse->getStatusCode(), + $response->baseResponse->headers->all() + ) + ); + + $this->assertEmpty(json_decode($response->getContent())); + } } diff --git a/tests/Http/Controllers/Api/Volumes/FilterVideoAnnotationsByLabelControllerTest.php b/tests/Http/Controllers/Api/Volumes/FilterVideoAnnotationsByLabelControllerTest.php index dfd087e..97e0738 100644 --- a/tests/Http/Controllers/Api/Volumes/FilterVideoAnnotationsByLabelControllerTest.php +++ b/tests/Http/Controllers/Api/Volumes/FilterVideoAnnotationsByLabelControllerTest.php @@ -3,11 +3,15 @@ namespace Biigle\Tests\Modules\Largo\Http\Controllers\Api\Volumes; use ApiTestCase; +use Carbon\Carbon; +use Biigle\Tests\LabelTest; +use Biigle\Tests\VideoTest; +use Biigle\Tests\ProjectTest; +use Illuminate\Testing\TestResponse; +use Biigle\Tests\VideoAnnotationTest; use Biigle\Tests\AnnotationSessionTest; use Biigle\Tests\VideoAnnotationLabelTest; -use Biigle\Tests\VideoAnnotationTest; -use Biigle\Tests\VideoTest; -use Carbon\Carbon; +use Symfony\Component\HttpFoundation\Response; class FilterVideoAnnotationsByLabelControllerTest extends ApiTestCase { @@ -188,4 +192,85 @@ public function testIndexDuplicate() ->assertStatus(200) ->assertExactJson([$a1->id => $video->uuid]); } + + public function testGetVolumeAnnotationLabels() + { + $id = $this->volume()->id; + $project = ProjectTest::create(); + $v1 = VideoTest::create(['volume_id' => $id, 'filename' => 'abc.jpg']); + $v2 = VideoTest::create(['volume_id' => $id, 'filename' => 'def.jpg']); + $a1 = VideoAnnotationTest::create(['video_id' => $v1]); + $a2 = VideoAnnotationTest::create(['video_id' => $v2->id]); + $l1 = LabelTest::create(); + $l2 = LabelTest::create(); + VideoAnnotationLabelTest::create(['annotation_id' => $a1->id, 'label_id' => $l1->id]); + VideoAnnotationLabelTest::create(['annotation_id' => $a2->id, 'label_id' => $l2->id]); + + $project->volumes()->attach($id); + + $this->doTestApiRoute('GET', "/api/v1/volume/{$id}/video-annotations"); + + $this->beUser(); + $this->getJson("/api/v1/volume/{$id}/video-annotations") + ->assertStatus(403); + + $this->beEditor(); + $response = $this->getJson("/api/v1/volume/{$id}/video-annotations")->assertStatus(200); + + ob_start(); + $response->sendContent(); + $content = ob_get_clean(); + $response = new TestResponse( + new Response( + $content, + $response->baseResponse->getStatusCode(), + $response->baseResponse->headers->all() + ) + ); + + $response->assertJsonFragment([ + 'uuid' => $v1->uuid, + 'annotation_id' => $a1->id, + 'label_id' => $l1->id, + 'label_tree_id' => $l1->label_tree_id + ]) + ->assertJsonFragment([ + 'uuid' => $v2->uuid, + 'annotation_id' => $a2->id, + 'label_id' => $l2->id, + 'label_tree_id' => $l2->label_tree_id + ]); + + $this->assertCount(2, json_decode($response->getContent())); + } + + public function testGetVolumeWithoutAnnotationsAnnotationLabels() + { + $id = $this->volume()->id; + $project = ProjectTest::create(); + VideoTest::create(['volume_id' => $this->volume()->id]); + $project->volumes()->attach($id); + + $this->doTestApiRoute('GET', "/api/v1/volume/{$id}/video-annotations"); + + $this->beUser(); + $this->getJson("/api/v1/volume/{$id}/video-annotations") + ->assertStatus(403); + + $this->beEditor(); + $response = $this->getJson("/api/v1/volume/{$id}/video-annotations")->assertStatus(200); + + ob_start(); + $response->sendContent(); + $content = ob_get_clean(); + $response = new TestResponse( + new Response( + $content, + $response->baseResponse->getStatusCode(), + $response->baseResponse->headers->all() + ) + ); + + $this->assertEmpty(json_decode($response->getContent())); + } }