diff --git a/src/Http/Controllers/Api/Projects/SortAnnotationsByOutliersController.php b/src/Http/Controllers/Api/Projects/SortAnnotationsByOutliersController.php index f748e665..a15c7f33 100644 --- a/src/Http/Controllers/Api/Projects/SortAnnotationsByOutliersController.php +++ b/src/Http/Controllers/Api/Projects/SortAnnotationsByOutliersController.php @@ -4,8 +4,6 @@ use DB; use Biigle\Http\Controllers\Api\Controller; -use Biigle\Modules\Largo\ImageAnnotationLabelFeatureVector; -use Biigle\Modules\Largo\VideoAnnotationLabelFeatureVector; use Biigle\Project; class SortAnnotationsByOutliersController extends Controller @@ -30,7 +28,7 @@ public function index($pid, $lid) $project = Project::findOrFail($pid); $this->authorize('access', $project); - // This was too complicated with the query builder. Since there is no rist of SQL + // This was too complicated with the query builder. Since there is no risk of SQL // injection here, we just use raw SQL. $sql = << $pid, 'lid' => $lid]); - // Filtering unique IDs is not required here because the UNIQUE in the query + // Filtering unique IDs is not required here because the UNION in the query // takes care of that. return array_map(fn ($v) => $v->id, $ids); } diff --git a/src/Http/Controllers/Api/Projects/SortAnnotationsBySimilarityController.php b/src/Http/Controllers/Api/Projects/SortAnnotationsBySimilarityController.php new file mode 100644 index 00000000..d4763f62 --- /dev/null +++ b/src/Http/Controllers/Api/Projects/SortAnnotationsBySimilarityController.php @@ -0,0 +1,67 @@ +reference; + + // This was too complicated with the query builder. Since there is no risk of SQL + // injection here, we just use raw SQL. + $sql = << :vector, id DESC + SQL; + + $ids = DB::select($sql, [ + 'pid' => $request->project->id, + 'lid' => $r->label_id, + 'vector' => $r->vector, + // We need only one at a time but since the ID 0 never exists, we just take + // it as the other ID. + 'iid' => $request->input('image_annotation_id', 0), + 'vid' => $request->input('video_annotation_id', 0), + ]); + + // Filtering unique IDs is not required here because the UNION in the query + // takes care of that. + return array_map(fn ($v) => $v->id, $ids); + } +} diff --git a/src/Http/Controllers/Api/Volumes/SortAnnotationsBySimilarityController.php b/src/Http/Controllers/Api/Volumes/SortAnnotationsBySimilarityController.php new file mode 100644 index 00000000..1264cead --- /dev/null +++ b/src/Http/Controllers/Api/Volumes/SortAnnotationsBySimilarityController.php @@ -0,0 +1,49 @@ +reference; + + if ($request->volume->isVideoVolume()) { + $query = VideoAnnotationLabelFeatureVector::where('volume_id', $r->volume_id) + ->where('label_id', $r->label_id) + ->where('id', '!=', $r->id) + ->orderByRaw('vector <=> ?, annotation_id DESC', [$r->vector]); + } else { + $query = ImageAnnotationLabelFeatureVector::where('volume_id', $r->volume_id) + ->where('label_id', $r->label_id) + ->where('id', '!=', $r->id) + ->orderByRaw('vector <=> ?, annotation_id DESC', [$r->vector]); + } + + return $query->pluck('annotation_id') + // Use distinct/unique *after* fetchig from the DB because otherwise the + // vector would need to be selected, too (order by expressions must appear in + // the select list). But we don't want to get the huge vectors. + ->unique() + ->values(); + } +} diff --git a/src/Http/Requests/IndexProjectAnnotationsSimilarity.php b/src/Http/Requests/IndexProjectAnnotationsSimilarity.php new file mode 100644 index 00000000..2ddf675e --- /dev/null +++ b/src/Http/Requests/IndexProjectAnnotationsSimilarity.php @@ -0,0 +1,87 @@ +project = Project::findOrFail($this->route('id')); + + return $this->user()->can('access', $this->project); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + return [ + 'label_id' => 'required|bail|integer', + 'image_annotation_id' => 'required_without:video_annotation_id|bail|integer', + 'video_annotation_id' => 'required_without:image_annotation_id|bail|integer', + ]; + } + + /** + * Configure the validator instance. + * + * @param \Illuminate\Validation\Validator $validator + * @return void + */ + public function withValidator($validator) + { + $validator->after(function ($validator) { + if ($validator->errors()->isNotEmpty()) { + return; + } + + $ids = $this->project->volumes()->pluck('id'); + if ($this->input('image_annotation_id')) { + $this->reference = ImageAnnotationLabelFeatureVector::whereIn('volume_id', $ids) + ->where('label_id', $this->input('label_id')) + ->where('annotation_id', $this->input('image_annotation_id')) + ->first(); + + if (is_null($this->reference)) { + $validator->errors()->add('image_annotation_id', 'The annotation does not exist in the project.'); + } + } else { + $this->reference = VideoAnnotationLabelFeatureVector::whereIn('volume_id', $ids) + ->where('label_id', $this->input('label_id')) + ->where('annotation_id', $this->input('video_annotation_id')) + ->first(); + + if (is_null($this->reference)) { + $validator->errors()->add('video_annotation_id', 'The annotation does not exist in the project.'); + } + } + + }); + } +} diff --git a/src/Http/Requests/IndexVolumeAnnotationsSimilarity.php b/src/Http/Requests/IndexVolumeAnnotationsSimilarity.php new file mode 100644 index 00000000..0792a8bd --- /dev/null +++ b/src/Http/Requests/IndexVolumeAnnotationsSimilarity.php @@ -0,0 +1,76 @@ +volume = Volume::findOrFail($this->route('id')); + + return $this->user()->can('access', $this->volume); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + return [ + 'label_id' => 'required|bail|integer', + 'annotation_id' => 'required|bail|integer', + ]; + } + + /** + * Configure the validator instance. + * + * @param \Illuminate\Validation\Validator $validator + * @return void + */ + public function withValidator($validator) + { + $validator->after(function ($validator) { + if ($this->volume->isImageVolume()) { + $this->reference = ImageAnnotationLabelFeatureVector::where('volume_id', $this->volume->id) + ->where('label_id', $this->input('label_id')) + ->where('annotation_id', $this->input('annotation_id')) + ->first(); + } else { + $this->reference = VideoAnnotationLabelFeatureVector::where('volume_id', $this->volume->id) + ->where('label_id', $this->input('label_id')) + ->where('annotation_id', $this->input('annotation_id')) + ->first(); + } + + if (is_null($this->reference)) { + $validator->errors()->add('annotation_id', 'The annotation does not exist in the volume.'); + } + }); + } +} diff --git a/src/Http/routes.php b/src/Http/routes.php index ad39aa3d..4edf1ebe 100644 --- a/src/Http/routes.php +++ b/src/Http/routes.php @@ -37,6 +37,10 @@ 'uses' => 'Projects\SortAnnotationsByOutliersController@index', ]); + $router->get('projects/{id}/annotations/sort/similarity', [ + 'uses' => 'Projects\SortAnnotationsBySimilarityController@index', + ]); + $router->get('projects/{id}/image-annotations/filter/label/{id2}', [ 'uses' => 'Projects\FilterImageAnnotationsByLabelController@index', ]); @@ -53,6 +57,10 @@ 'uses' => 'Volumes\SortAnnotationsByOutliersController@index', ]); + $router->get('volumes/{id}/annotations/sort/similarity', [ + 'uses' => 'Volumes\SortAnnotationsBySimilarityController@index', + ]); + $router->get('volumes/{id}/image-annotations/examples/{id2}', [ 'uses' => 'Volumes\ImageAnnotationExamplesController@index', ]); diff --git a/src/public/assets/scripts/main.js b/src/public/assets/scripts/main.js index 7b2c2e29..71fd61a2 100644 --- a/src/public/assets/scripts/main.js +++ b/src/public/assets/scripts/main.js @@ -1 +1 @@ -(()=>{"use strict";var t,e={235:()=>{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 d=u.render;u.render=function(t,e){return l.call(e),d(t,e)}}else{var c=u.beforeCreate;u.beforeCreate=c?[].concat(c,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}"}});var a=biigle.$require("echo"),r=biigle.$require("events"),l=biigle.$require("messages").handleErrorResponse,u=biigle.$require("volumes.components.imageGrid"),d=biigle.$require("volumes.components.imageGridImage"),c=biigle.$require("annotations.components.labelsTabPlugins"),h=biigle.$require("labelTrees.components.labelTrees"),g=biigle.$require("core.mixins.loader"),m=biigle.$require("messages"),f=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 w=n({mixins:[g],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},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!==k||this.sortingDirection!==E)}},methods:{getAnnotations:function(t){var e,n,i,s,o=this;this.annotationsCache.hasOwnProperty(t.id)?i=Vue.Promise.resolve():(Vue.set(this.annotationsCache,t.id,[]),this.startLoading(),i=this.queryAnnotations(t).then((function(e){return o.gotAnnotations(t,e)}),l));var a=null===(e=this.sortingSequenceCache)||void 0===e||null===(n=e[t.id])||void 0===n?void 0:n[this.sortingKey];this.sortingIsActive&&!a?(this.loading||this.startLoading(),s=this.fetchSortingSequence(this.sortingKey,t.id).catch(l)):s=Vue.Promise.resolve(),Vue.Promise.all([i,s]).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(),m.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(),m.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=this;return(t===D?this.querySortByOutlier(e).then((function(t){return t.body})):Vue.Promise.resolve([])).then((function(i){return n.putSortingSequenceToCache(t,e,i)}))},putSortingSequenceToCache:function(t,e,n){this.sortingSequenceCache[e]||Vue.set(this.sortingSequenceCache,e,{}),this.sortingSequenceCache[e][t]=n},updateSortKey:function(t){var e,n,i,s=this,o=null===(e=this.selectedLabel)||void 0===e?void 0:e.id,a=null===(n=this.sortingSequenceCache)||void 0===n||null===(i=n[o])||void 0===i?void 0:i[t];o&&!a?(this.startLoading(),this.fetchSortingSequence(t,o).then((function(){return s.sortingKey=t})).catch(l).finally(this.finishLoading)):this.sortingKey=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(){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:[R],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])}},created:function(){var t=biigle.$require("annotationCatalog.labelTree");this.labelTrees=[t]}},undefined,undefined,!1,null,null,null).exports;const j=n({mixins:[R],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){var e=this;return o.sortAnnotationsByOutlier({id:this.volumeId,label_id:t}).then((function(t){return"image"===e.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 B=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}"}});const G=n({mixins:[R],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})}},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",B),biigle.$mount("project-largo-container",G)},401:()=>{}},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(d=0;d=o)&&Object.keys(i.O).every((t=>i.O[t](n[l])))?n.splice(l--,1):(r=!1,o0&&t[d-1][2]>o;d--)t[d]=t[d-1];t[d]=[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 d=l(i)}for(e&&e(n);ui(235)));var s=i.O(void 0,[392],(()=>i(401)));s=i.O(s)})(); \ No newline at end of file +(()=>{"use strict";var t,e={67:()=>{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"),b=biigle.$require("annotations.components.settingsTabPlugins"),v=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)&&Array.prototype.push.apply(t,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===B}},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)),n=this.sortingKey===B?this.resetSorting():this.sortingIsActive?this.updateSortKey(this.sortingKey):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(t===k)s=this.querySortByOutlier(e).then((function(t){return t.body}));else{if(t===B)return this.querySortBySimilarity(e,this.similarityReference).then((function(t){return t.body}));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!==B&&(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===B&&(n.needsSimilarityReference=!1,n.pinnedImage=n.similarityReference)})).catch((function(t){n.handleErrorResponse(t),n.similarityReference=null})).finally(this.finishLoading)},handleInitSimilaritySort:function(){this.sortingKey!==B&&(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(B))},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])}},created:function(){var t=biigle.$require("annotationCatalog.labelTree");this.labelTrees=[t]}},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)},401:()=>{}},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(67)));var s=i.O(void 0,[392],(()=>i(401)));s=i.O(s)})(); \ No newline at end of file diff --git a/src/public/assets/styles/main.css b/src/public/assets/styles/main.css index e4146932..fa538f45 100644 --- a/src/public/assets/styles/main.css +++ b/src/public/assets/styles/main.css @@ -1 +1 @@ -.navbar-text.navbar-largo-breadcrumbs,.navbar-text.navbar-largo-breadcrumbs a{color:#ccc}.largo-images__alerts{align-items:center;display:flex;height:100%;justify-content:center;left:0;pointer-events:none;position:absolute;top:0;transition:background-color .25s ease;width:100%;z-index:2}.largo-images__alerts.block{background-color:rgba(0,0,0,.5);pointer-events:auto}.sidebar__tab--open.largo-tab{display:flex!important;flex-direction:column}.sidebar__tab--open.largo-tab .largo-tab__button{padding-bottom:10px}.sidebar__tab--open.largo-tab .largo-tab__label-trees{flex:1;overflow:auto}.image-grid__image--largo .outlines{height:calc(100% - 4px);left:2px;pointer-events:none;position:absolute;top:2px;width:calc(100% - 4px)}.image-grid__image--catalog>a{width:100%}.image-grid__image--relabel .new-label{bottom:0;left:0;overflow-x:hidden;padding:.5em;pointer-events:none;position:absolute;text-align:center;text-overflow:ellipsis;white-space:nowrap;width:100%}.image-grid__image--relabel .new-label__name{padding-right:16px}.image-grid__image--relabel .new-label__color{border:2px solid #eee;border-radius:50%;display:inline-block;height:1em;vertical-align:middle;width:1em}.largo-example-annotations .largo-example-annotations__images{margin-right:-1em;overflow-x:auto;white-space:nowrap}.largo-example-annotations .largo-example-annotation{image-orientation:none;height:125px;width:auto}.largo-example-annotations .largo-example-annotation:not(:last-child){margin-right:.5em}.largo-example-annotations .alert{margin-bottom:0} +.navbar-text.navbar-largo-breadcrumbs,.navbar-text.navbar-largo-breadcrumbs a{color:#ccc}.largo-images__alerts{align-items:center;display:flex;height:100%;justify-content:center;left:0;pointer-events:none;position:absolute;top:0;transition:background-color .25s ease;width:100%;z-index:2}.largo-images__alerts.block{background-color:rgba(0,0,0,.5);pointer-events:auto}.sidebar__tab--open.largo-tab{display:flex!important;flex-direction:column}.sidebar__tab--open.largo-tab .largo-tab__button{padding-bottom:10px}.sidebar__tab--open.largo-tab .largo-tab__label-trees{flex:1;overflow:auto}.image-grid__image--largo .outlines{height:calc(100% - 4px);left:2px;pointer-events:none;position:absolute;top:2px;width:calc(100% - 4px)}.image-grid__image--largo .image-buttons-bottom{bottom:6px;position:absolute;right:6px}.image-grid__image--catalog>a{width:100%}.image-grid__image--relabel .new-label{bottom:0;left:0;overflow-x:hidden;padding:.5em;pointer-events:none;position:absolute;text-align:center;text-overflow:ellipsis;white-space:nowrap;width:100%}.image-grid__image--relabel .new-label__name{padding-right:16px}.image-grid__image--relabel .new-label__color{border:2px solid #eee;border-radius:50%;display:inline-block;height:1em;vertical-align:middle;width:1em}.largo-example-annotations .largo-example-annotations__images{margin-right:-1em;overflow-x:auto;white-space:nowrap}.largo-example-annotations .largo-example-annotation{image-orientation:none;height:125px;width:auto}.largo-example-annotations .largo-example-annotation:not(:last-child){margin-right:.5em}.largo-example-annotations .alert{margin-bottom:0} diff --git a/src/public/mix-manifest.json b/src/public/mix-manifest.json index ab8337a3..efdc1799 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=9d428530385ad8ea4cbd82e4600d8cd0", - "/assets/styles/main.css": "/assets/styles/main.css?id=f5673570cda3cacb644da42a2840c218" + "/assets/scripts/main.js": "/assets/scripts/main.js?id=608af5558117398ca4c3d3598a905abb", + "/assets/styles/main.css": "/assets/styles/main.css?id=5295955359904ac00dbeabaf613c5792" } diff --git a/src/resources/assets/js/api/projects.js b/src/resources/assets/js/api/projects.js index 5cbb196e..2c9d31d2 100644 --- a/src/resources/assets/js/api/projects.js +++ b/src/resources/assets/js/api/projects.js @@ -27,4 +27,8 @@ export default Vue.resource('api/v1/projects{/id}/largo', {}, { method: 'GET', url: 'api/v1/projects{/id}/annotations/sort/outliers{/label_id}', }, + sortAnnotationsBySimilarity: { + method: 'GET', + url: 'api/v1/projects{/id}/annotations/sort/similarity', + }, }); diff --git a/src/resources/assets/js/api/volumes.js b/src/resources/assets/js/api/volumes.js index 62542e72..cd27cdf4 100644 --- a/src/resources/assets/js/api/volumes.js +++ b/src/resources/assets/js/api/volumes.js @@ -35,4 +35,8 @@ export default Vue.resource('api/v1/volumes{/id}/largo', {}, { method: 'GET', url: 'api/v1/volumes{/id}/annotations/sort/outliers{/label_id}', }, + sortAnnotationsBySimilarity: { + method: 'GET', + url: 'api/v1/volumes{/id}/annotations/sort/similarity', + }, }); diff --git a/src/resources/assets/js/components/dismissImageGridImage.vue b/src/resources/assets/js/components/dismissImageGridImage.vue index 03187fd0..6b3828b2 100644 --- a/src/resources/assets/js/components/dismissImageGridImage.vue +++ b/src/resources/assets/js/components/dismissImageGridImage.vue @@ -1,10 +1,26 @@ @@ -64,14 +88,23 @@ export const SORT_DIRECTION = { export const SORT_KEY = { ANNOTATION_ID: 0, OUTLIER: 1, + SIMILARITY: 2, }; export default { - data() { - return { - sortDirection: SORT_DIRECTION.DESCENDING, - sortKey: SORT_KEY.ANNOTATION_ID, - }; + props: { + sortKey: { + type: Number, + required: true, + }, + sortDirection: { + type: Number, + required: true, + }, + needsSimilarityReference: { + type: Boolean, + default: false, + }, }, computed: { sortedAscending() { @@ -86,31 +119,40 @@ export default { sortingByOutlier() { return this.sortKey === SORT_KEY.OUTLIER; }, + sortingBySimilarity() { + return this.sortKey === SORT_KEY.SIMILARITY; + }, }, methods: { sortAscending() { - this.sortDirection = SORT_DIRECTION.ASCENDING; + this.$emit('change-direction', SORT_DIRECTION.ASCENDING); }, sortDescending() { - this.sortDirection = SORT_DIRECTION.DESCENDING; + this.$emit('change-direction', SORT_DIRECTION.DESCENDING); }, reset() { this.sortDescending(); this.sortByAnnotationId(); }, sortByAnnotationId() { - this.sortKey = SORT_KEY.ANNOTATION_ID; + this.$emit('change-key', SORT_KEY.ANNOTATION_ID); + + if (this.needsSimilarityReference) { + this.cancelSortBySimilarity(); + } }, sortByOutlier() { - this.sortKey = SORT_KEY.OUTLIER; + this.$emit('change-key', SORT_KEY.OUTLIER); + + if (this.needsSimilarityReference) { + this.cancelSortBySimilarity(); + } }, - }, - watch: { - sortDirection(direction) { - this.$emit('change-direction', direction); + initializeSortBySimilarity() { + this.$emit('init-similarity'); }, - sortKey(key) { - this.$emit('change-key', key); + cancelSortBySimilarity() { + this.$emit('cancel-similarity'); }, }, }; diff --git a/src/resources/assets/js/largoContainer.vue b/src/resources/assets/js/largoContainer.vue index c5e8b3c0..949ffffc 100644 --- a/src/resources/assets/js/largoContainer.vue +++ b/src/resources/assets/js/largoContainer.vue @@ -33,18 +33,23 @@ export default { }, querySortByOutlier(labelId) { return VolumesApi.sortAnnotationsByOutlier({id: this.volumeId, label_id: labelId}) - .then((response) => { - // The sorting expects annotation IDs prefixed with 'i' or 'v' so it - // can work with mixed image and video annotations. - if (this.mediaType === 'image') { - response.body = response.body.map(id => 'i' + id); - } else { - response.body = response.body.map(id => 'v' + id); - } - - return response; - }); + .then(this.parseSortingQuery); + }, + querySortBySimilarity(labelId, reference) { + return VolumesApi.sortAnnotationsBySimilarity({id: this.volumeId, label_id: labelId, annotation_id: reference.id}) + .then(this.parseSortingQuery); }, + parseSortingQuery(response) { + // The sorting expects annotation IDs prefixed with 'i' or 'v' so it + // can work with mixed image and video annotations. + if (this.mediaType === 'image') { + response.body = response.body.map(id => 'i' + id); + } else { + response.body = response.body.map(id => 'v' + id); + } + + return response; + } }, created() { this.volumeId = biigle.$require('largo.volumeId'); diff --git a/src/resources/assets/js/mixins/largoContainer.vue b/src/resources/assets/js/mixins/largoContainer.vue index e5f011e5..c66488b9 100644 --- a/src/resources/assets/js/mixins/largoContainer.vue +++ b/src/resources/assets/js/mixins/largoContainer.vue @@ -45,8 +45,12 @@ export default { // The second level key is the sorting key. The cached value is an array // of annotation IDs sorted in ascending order. sortingSequenceCache: {}, + sortingSequence: [], sortingDirection: SORT_DIRECTION.DESCENDING, sortingKey: SORT_KEY.ANNOTATION_ID, + needsSimilarityReference: false, + similarityReference: null, + pinnedImage: null, }; }, provide() { @@ -75,25 +79,26 @@ export default { return []; }, sortedAnnotations() { - let annotations = this.annotations.slice(); + let annotations = this.annotations; - // This will always be missing for the default sorting. - const sequence = this.sortingSequenceCache?.[this.selectedLabel?.id]?.[this.sortingKey]; + if (annotations.length === 0) { + return annotations; + } - if (sequence) { + // This will be empty for the default sorting. + if (this.sortingSequence.length > 0) { const map = {}; - sequence.forEach((id, idx) => map[id] = idx); + annotations.forEach((a) => { + // Image annotation IDs are prefixed with 'i', video annotations with + // 'v' to avoid duplicate IDs whe sorting both types of annotations. + map[a.type === VIDEO_ANNOTATION ? ('v' + a.id) : ('i' + a.id)] = a; + }); - // Image annotation IDs are prefixed with 'i', video annotations with - // 'v' to avoid duplicate IDs whe sorting both types of annotations. - annotations.sort((a, b) => - map[a.type === VIDEO_ANNOTATION ? ('v' + a.id) : ('i' + a.id)] - - map[b.type === VIDEO_ANNOTATION ? ('v' + b.id) : ('i' + b.id)] - ); + annotations = this.sortingSequence.map(id => map[id]); } if (this.sortingDirection === SORT_DIRECTION.ASCENDING) { - return annotations.reverse(); + return annotations.slice().reverse(); } return annotations; @@ -148,6 +153,9 @@ export default { sortingIsActive() { return this.isInDismissStep && (this.sortingKey !== SORT_KEY.ANNOTATION_ID || this.sortingDirection !== SORT_DIRECTION.DESCENDING); }, + imagesPinnable() { + return this.needsSimilarityReference || this.sortingKey === SORT_KEY.SIMILARITY; + }, }, methods: { getAnnotations(label) { @@ -163,13 +171,11 @@ export default { promise1 = Vue.Promise.resolve(); } - const sequence = this.sortingSequenceCache?.[label.id]?.[this.sortingKey]; - if (this.sortingIsActive && !sequence) { - if (!this.loading) { - this.startLoading(); - } - promise2 = this.fetchSortingSequence(this.sortingKey, label.id) - .catch(handleErrorResponse); + if (this.sortingKey === SORT_KEY.SIMILARITY) { + promise2 = this.resetSorting(); + } else if (this.sortingIsActive) { + // Reload sequence for new label. + promise2 = this.updateSortKey(this.sortingKey); } else { promise2 = Vue.Promise.resolve(); } @@ -387,10 +393,19 @@ export default { this.sortingDirection = direction; }, fetchSortingSequence(key, labelId) { + const sequence = this.sortingSequenceCache?.[labelId]?.[key]; + if (sequence) { + return Vue.Promise.resolve(sequence); + } + let promise; if (key === SORT_KEY.OUTLIER) { promise = this.querySortByOutlier(labelId) .then(response => response.body); + } else if (key === SORT_KEY.SIMILARITY) { + // Skip cacheing for this sorting method. + return this.querySortBySimilarity(labelId, this.similarityReference) + .then(response => response.body); } else { promise = Vue.Promise.resolve([]); } @@ -403,20 +418,52 @@ export default { } this.sortingSequenceCache[labelId][key] = sequence; + + return sequence; }, updateSortKey(key) { + if (key !== SORT_KEY.SIMILARITY) { + this.similarityReference = null; + this.pinnedImage = null; + } + const labelId = this.selectedLabel?.id; - const sequence = this.sortingSequenceCache?.[labelId]?.[key]; - if (labelId && !sequence) { - this.startLoading(); - this.fetchSortingSequence(key, labelId) - .then(() => this.sortingKey = key) - .catch(handleErrorResponse) - .finally(this.finishLoading); - } else { - this.sortingKey = key; + this.startLoading(); + return this.fetchSortingSequence(key, labelId) + .then((sequence) => { + this.sortingKey = key; + this.sortingSequence = sequence; + if (key === SORT_KEY.SIMILARITY) { + this.needsSimilarityReference = false; + this.pinnedImage = this.similarityReference; + } + }) + .catch((r) => { + this.handleErrorResponse(r); + this.similarityReference = null; + }) + .finally(this.finishLoading); + }, + handleInitSimilaritySort() { + if (this.sortingKey !== SORT_KEY.SIMILARITY) { + this.needsSimilarityReference = true; + } + }, + handleCancelSimilaritySort() { + this.needsSimilarityReference = false; + }, + handlePinImage(image) { + if (this.pinnedImage?.id === image.id) { + this.resetSorting(); + } else if (this.imagesPinnable) { + this.similarityReference = image; + this.updateSortKey(SORT_KEY.SIMILARITY); } }, + resetSorting() { + return this.updateSortKey(SORT_KEY.ANNOTATION_ID) + .then(() => this.sortingDirection = SORT_DIRECTION.DESCENDING); + }, }, watch: { annotations(annotations) { diff --git a/src/resources/assets/js/projectLargoContainer.vue b/src/resources/assets/js/projectLargoContainer.vue index ba221c5b..c3c03ec2 100644 --- a/src/resources/assets/js/projectLargoContainer.vue +++ b/src/resources/assets/js/projectLargoContainer.vue @@ -1,6 +1,7 @@