Merge pull request #4106 from solgenomics/topic/wishlist
[sgn.git] / mason / tools / image_analysis.mas
bloba0b51b610121b95cf5867d3aa2e4b2a18bc65b3b
2 <%args>
4 </%args>
6 <& /util/import_javascript.mas,
7   classes => ['jquery.dataTables-buttons-min',
8       'jquery.iframe-post-form',
9       'jszip-min', 'pdfmake.pdfmake-min',
10       'pdfmake.vfs_fonts',
11       'buttons.bootstrap-min',
12       'buttons.html5-min',
13       'jquery',
14       'jquery.cookie',
15       'thickbox',
16       'CXGN.BreedersToolbox.HTMLSelect',
17       'd3.d3v4Min'
18   ]
21 <style>
22 td.details-control {
23     text-align:center;
24     color:forestgreen;
25     cursor: pointer;
27 tr.shown td.details-control {
28     text-align:center;
29     color:red;
32 .bar {
33   fill: steelblue;
34   shape-rendering: crispEdges;
36 </style>
38 <link rel="stylesheet" type="text/css" href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" />
40 <& /page/page_title.mas, title=>"Image Analysis" &>
41 <hr>
43 <div class="well well-sm">
44     <center>
45     <h4>The Necrosis Image Analysis is explained in the paper here: <a href="https://csce.ucmss.com/cr/books/2018/LFS/CSREA2018/IPC3638.pdf">Necrosis Image Analysis</a>.</h4>
46     </center>
47 </div>
49 <div class="well well-sm">
50     <center>
51     <button class="btn btn-primary" id="upload_images_link">Upload New Images</button>
52     </center>
53 </div>
54 <& /breeders_toolbox/upload_images.mas &>
55 <& /breeders_toolbox/trial/create_spreadsheet_dialog.mas &>
57 <&| /page/info_section.mas, title=>"Image Search Criteria",  collapsible => 1, collapsed=>0, subtitle => "All images may not have names, descriptions, or tags associated with them."&>
59     <div id="image_search_form" class="well well-sm">
60         <div class="form-horizontal" >
61             <div class="form-group">
62                 <label class="col-sm-3 control-label">Select a Field Trial: </label>
63                 <div class="col-sm-9" >
64                     <div id ="image_analysis_trial_select">
65                     </div>
66                 </div>
67             </div>
68             <div class="form-group">
69                 <label class="col-sm-6 control-label">Image descriptors (name, description, or filename): </label>
70                 <div class="col-sm-6" >
71                     <input class="form-control" type="text" id="image_description_filename_composite" name="image_description_filename_composite" placeholder="e.g. MyImageName" />
72                 </div>
73             </div>
74             <div class="form-group">
75                 <label class="col-sm-6 control-label">Submitter: </label>
76                 <div class="col-sm-6" >
77                     <input class="form-control" type="text" id="image_submitter" name="image_submitter" placeholder="e.g. JaneDoe" />
78                 </div>
79             </div>
80             <div class="form-group">
81                 <label class="col-sm-6 control-label">Image tag: </label>
82                 <div class="col-sm-6" >
83                     <input class="form-control" type="text" id="image_tag" name="image_tag" placeholder="e.g. ImageTagName" />
84                 </div>
85             </div>
86             <div class="form-group">
87                 <label class="col-sm-6 control-label">Associated stock: </label>
88                 <div class="col-sm-6" >
89                     <input class="form-control" type="text" id="image_stock_uniquename" name="image_stock_uniquename" placeholder="e.g. FieldPlot100" />
90                 </div>
91             </div>
92         </div>
94         <center>
95         <button class="btn btn-primary" id="image_search_submit" >Search</button>
96         </center>
97     </div>
98 </&>
100 <&| /page/info_section.mas, title=>"Image Search Results",  collapsible => 1, collapsed=>0 &>
101     <div class="well well-sm">
102         <div class="panel panel-default">
103             <div class="panel-body">
104                 <table id="image_analysis_image_search_results" class="table table-hover table-striped">
105                 <thead>
106                   <tr>
107                     <th>Select</th>
108                     <th>Image Thumbnail</th>
109                     <th>Filename</th>
110                     <th>Description</th>
111                     <th>Submitter</th>
112                     <th>Associations</th>
113                     <th>Observations</th>
114                     <th>Tags</th>
115                 </tr>
116                 </thead>
117                 </table>
118             </div>
119         </div>
120     </div>
121 </&>
123 <&| /page/info_section.mas, title=>"Image Analysis", collapsible => 1, collapsed=>0 &>
124     <div class="well well-sm">
125         <div class="panel panel-default">
126             <div class="panel-body">
127                 <div class="form-group">
128                     <label class="col-sm-6 control-label">Image Analysis Service: </label>
129                     <div class="col-sm-6" >
130                         <select class="form-control" id="image_analysis_service_select" name="image_analysis_service_select">
131                             <option value="">Select An Analysis Service</option>
132                             <option value="necrosis">Necrosis(Makerere AIR Lab)</option>
133                             <option value="largest_contour_percent">Necrosis Largest Contour Mask Percent</option>
134                             <option value="count_contours">Count Contours</option>
135                             <option value="count_sift">SIFT Feature Count</option>
136                             <option value="whitefly_count">Whitefly Count (Makerere AIR Lab)</option>
137                         </select>
138                     </div>
139                 </div>
140                 <br/>
141                 <div class="form-group" id="image_analysis_trait_group" style="display: none;">
142                     <label class="col-sm-6 control-label">Trait to be Analyzed: </label>
143                     <div class="col-sm-6" >
144                         <select class="form-control" id="image_analysis_trait_select" name="image_analysis_service_select"></select>
145                     </div>
146                 </div>
147                 <br/><br/><br/>
149                 <hr>
150                 <center>
151                 <button class="btn btn-primary" id="image_analysis_submit" disabled>Submit for Analysis</button>
152                 </center>
153                 <hr>
154                 <div id="image_analysis_result" style="display: none;">
155                     <table class="display" style="width:100%" id="image_analysis_result_table">
156                         <thead>
157                            <tr>
158                                <th></th>
159                                <th>Stock</th>
160                                <th>Trait</th>
161                                <th># Analyzed Images</th>
162                                <th>Mean Value</th>
163                            </tr>
164                        </thead>
165                     </table>
166                     <hr>
167                     <center><button class="btn btn-primary" id="image_analysis_save_results">Save Results</button></center>
168                 </div>
169             </div>
170         </div>
171     </div>
173     <br/><br/>
174     <div class="well well-sm">
175         <div class="panel panel-default">
176             <div class="panel-body">
177                 <div class="form-group">
178                     <div class="col-sm-9">
179                     <label class="col-sm-6 control-label">Image Analysis Usage </label>
180                     </div>
181                     <div class="col-sm-3" >
182                         <label class="control-label">Date Range: </label>
183                         <select class="form-control" id="usage_range_select" name="usage_range_select">
184                             <option value="all">All Dates</option>
185                             <option value="year">Latest Year</option>
186                             <option value="month">Latest Month</option>
187                             <option value="week">Latest Week</option>
188                         </select>
189                     </div>
190                 </div>
191                 <br/>
192                 <div class="form-group" id="image_analysis_usage">
193                 </div>
194             </div>
195         </div>
196     </div>
198     <div class="modal fade" id="saveResultModal" tabindex="-1" role="dialog" aria-labelledby="saveResultModal" aria-hidden="true">
199       <div class="modal-dialog modal-dialog-centered" role="document">
200         <div class="modal-content">
201           <div class="modal-header">
202             <h5 class="modal-title" id="saveResultModalTitle">Image Analysis Save Status</h5>
203             <button type="button" class="close" data-dismiss="modal" aria-label="Close">
204               <span aria-hidden="true">&times;</span>
205             </button>
206           </div>
207           <div class="modal-body" id="saveResultModalBody">
208           </div>
209           <div class="modal-footer">
210             <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
211           </div>
212         </div>
213       </div>
214     </div>
215 </&>
217 <script>
219 var service_traits = {
220     'necrosis': {
221         "CBSDpct|CO_334:0002078" : "Cassava",
222     },
223     'largest_contour_percent': {},
224     'count_contours': {},
225     'count_sift': {},
226     'whitefly_count': {},
229 jQuery(document).ready(function(){
231     get_select_box('trials', 'image_analysis_trial_select', { 'name' : 'html_image_analysis_trial_select', 'id' : 'html_image_analysis_trial_select', 'multiple':0, 'size':10, 'trial_name_values':1 });
233     _load_image_search_results();
235     var date_range = jQuery('#usage_range_select').val();
236     _load_analysis_activity_graph(date_range);
238     jQuery('#image_search_submit').click(function(){
239         if (jQuery('#html_image_analysis_trial_select').val() == '') {
240             alert("Please select a Field Trial first!");
241             return false;
242         }
243         _load_image_search_results();
244     });
246     jQuery("#image_submitter").autocomplete({
247         source: '/ajax/people/autocomplete'
248     });
250     jQuery('#image_search_form').keypress( function( e ) {
251         var code = e.keyCode || e.which;
252         if( code == 13 ) {
253             jQuery('#image_search_submit').click();
254         }
255     });
257     jQuery('#image_analysis_image_search_results').on( 'draw.dt', function () {
258         jQuery('a.image_search_group').colorbox();
259     });$
261     jQuery('#image_analysis_service_select').change(function() {
262         var service = jQuery('#image_analysis_service_select').val();
263         jQuery('#image_analysis_trait_select').html('<option value="">Select A Trait</option>');
264         if (service) {
265             jQuery('#working_modal').modal('show');
266             // add crop and service specific trait options
267             var all_traits = service_traits[service];
268             var trait_names = Object.keys(all_traits);
269             var options =  [];
271             jQuery.ajax({
272                 url: '/brapi/v2/commoncropnames/',
273                 method: 'GET',
274                 success: function(response) {
275                     // console.log("Retrieved crop names and they are: "+response.result.data);
276                     var supportedcrops = response.result.data;
277                     supportedcrops.forEach(function(crop) {
278                         trait_names.forEach(function(name) {
279                             if (all_traits[name] == crop) {
280                                 // console.log("Id is "+id);
281                                 options.push('<option value="'+ name +'">'+ name +'</option>');
282                             }
283                         });
284                     });
285                     jQuery('#image_analysis_trait_select').append(options);
286                     jQuery('#image_analysis_trait_group').show();
287                     jQuery('#working_modal').modal('hide');
288                 },
289                 error: function(response) {
290                     console.log("error retrieving crop names: "+response);
291                     // just add all traits regardless of crop
292                     trait_names.forEach(function(name) {
293                         // console.log("Id is "+id);
294                         options.push('<option value="'+ name +'">'+ name +'</option>');
296                     });
297                     jQuery('#image_analysis_trait_select').append(options);
298                     jQuery('#image_analysis_trait_group').show();
299                     jQuery('#working_modal').modal('hide');
300                 }
301             });
302       }
303       else {
304           // hide trait select
305           jQuery('#image_analysis_trait_group').hide();
306       }
307     });
309     jQuery('#image_analysis_trait_select').change(function() {
310         if (jQuery('#image_analysis_trait_select').val()) {
311             jQuery('#image_analysis_submit').prop('disabled', false);
312         }
313         else {
314             jQuery('#image_analysis_submit').prop('disabled', true);
315         }
316     });
318     jQuery('#image_analysis_submit').click(function(){
319         var selected_image_ids = [];
320         jQuery('input[name="image_analysis_select"]').each(function() {
321             if (this.checked){
322                 selected_image_ids.push(this.value);
323             }
324         });
326         if (selected_image_ids.length < 1) {
327             alert('Please select at least one image first!');
328             return false;
329         }
331         var results = [];
332         var progress_modal = jQuery('#progress_modal');
333         var progress_bar = jQuery('#progress_bar');
334         var image_total = selected_image_ids.length;
335         var images_finished = 0;
336         var current_progress = 0;
337         progress_modal.modal('show');
338         jQuery('#progress_msg').text('Submitting images for analysis');
340         var deferred_calls = selected_image_ids.map(function(image_id, index) {
341             jQuery('#progress_msg').text('Submitting image '+index+' out of '+image_total+' images');
342             current_progress += (1 / image_total) * 10;
343             progress_bar.css("width", current_progress + "%")
344             .attr("aria-valuenow", current_progress)
345             .text(Math.round(current_progress) + "%");
347             var call = jQuery.ajax({
348                 url: '/ajax/image_analysis/submit',
349                 method: 'POST',
350                 data: {
351                     'selected_image_ids': image_id,
352                     'service': jQuery('#image_analysis_service_select').val(),
353                     'trait': jQuery('#image_analysis_trait_select').val(),
354                 },
355                 dataType:'json',
356                 success: function(response) {
357                     images_finished++;
358                     // console.log(response);
359                     jQuery('#progress_msg').text('Responses received for '+images_finished+' out of '+image_total+' images');
360                     current_progress += (1 / image_total) * 90;
361                     progress_bar.css("width", current_progress + "%")
362                     .attr("aria-valuenow", current_progress)
363                     .text(Math.round(current_progress) + "%");
364                     response.results.map(function(item) {
365                         if (item.result.error) {
366                             progress_modal.find('.modal-footer').append(
367                                 '<ul class="list-group"><li class="list-group-item list-group-item-danger"><span class="badge"><span class="glyphicon glyphicon-remove"></span></span>Error analyzing image number '+index+': '+item.result.error+'</li></ul>'
368                             );
369                         }
370                         else {
371                             // results.push(item);
372                         }
373                         results.push(item);
374                     });
375                 },
376                 error: function(response) {
377                     images_finished++;
378                     // console.log(response);
379                     jQuery('#progress_msg').text('Finished analyzing '+images_finished+' out of '+image_total+' images');
380                     current_progress += (1 / image_total) * 90
381                     progress_modal.find('.modal-footer').append(
382                         '<ul class="list-group"><li class="list-group-item list-group-item-danger"><span class="badge"><span class="glyphicon glyphicon-remove"></span></span>Error analyzing image number '+index+': '+response+'</li></ul>'
383                     );
384                 }
385             });
386             return call;
387         });
389         jQuery('#progress_msg').text('All '+image_total+' images submitted, waiting for responses.');
391         jQuery.when.apply(jQuery, deferred_calls).then(function() {
392             // console.log("Results are:");
393             // console.log(results);
394             if (results.length > 0) {
395                 jQuery.ajax({
396                     url: '/ajax/image_analysis/group',
397                     method: 'POST',
398                     data: { 'result': JSON.stringify(results) },
399                     success: function(response) {
400                         // console.log("Grouped Results are:");
401                         // console.log(response);
402                         current_progress = 100;
403                         progress_bar.css("width", current_progress + "%")
404                         .attr("aria-valuenow", current_progress)
405                         .text(Math.round(current_progress) + "%");
406                         jQuery('#progress_msg').text('Building results table.');
407                         jQuery('#image_analysis_result').show();
409                         var table = jQuery('#image_analysis_result_table').DataTable( {
410                             "destroy" : true,
411                             "data": response.results,
412                             "dom": 'Bfrtip',
413                             "buttons":  [
414                                 'copy', 'excel', 'csv', 'pdf'
415                             ],
416                             "columns": [
417                                 {
418                                     "className":      'details-control',
419                                     "orderable":      false,
420                                     "data":           null,
421                                     "defaultContent": '',
422                                     "render": function () {
423                                          return '<i class="fa fa-plus-square" aria-hidden="true"></i>';
424                                      },
425                                      width:"15px"
426                                 },
427                                 { "data": "observationUnitName" },
428                                 { "data": "observationVariableName" },
429                                 { "data": "numberAnalyzed" },
430                                 { "data": "value" }
431                             ],
432                             "order": [[1, 'asc']]
433                         } );
435                         // Add event listener for opening and closing details
436                         jQuery('#image_analysis_result_table tbody').on('click', 'td.details-control', function () {
437                             var tr = jQuery(this).closest('tr');
438                             var tdi = tr.find("i.fa");
439                             var row = table.row( tr );
441                             if ( row.child.isShown() ) {
442                                 // This row is already open - close it
443                                 row.child.hide();
444                                 tr.removeClass('shown');
445                                 tdi.first().removeClass('fa-minus-square');
446                                 tdi.first().addClass('fa-plus-square');
447                             }
448                             else {
449                                 // Open this row
450                                 row.child( format(row.data()) ).show();
451                                 tr.addClass('shown');
452                                 tdi.first().removeClass('fa-plus-square');
453                                 tdi.first().addClass('fa-minus-square');
454                             }
455                         } );
457                         setTimeout(function(){
458                             progress_modal.find('.modal-footer').html('');
459                             progress_modal.modal('hide');
460                         }, 1000);
462                     },
463                     error: function(response) {
464                         // jQuery('#working_modal').modal('hide');
465                         progress_modal.modal('hide');
466                         console.log('Error: '+response);
467                         // jQuery('#'+image_id).text('Error analyzing image number '+index+'. '+response);
468                         alert("An error occurred while displaying image analysis results.");
469                     }
470                 });
471             }
472             else {
473                 progress_modal.find('.modal-footer').html('');
474                 progress_modal.modal('hide');
475                 alert("No usable results returned from the service, aborting analysis.");                 // alert("No usable results returned from the service, aborting analysis.");
476             }
477         });
478     });
480     jQuery('#image_analysis_save_results').click(function(){
482         var table_data = jQuery('#image_analysis_result_table').DataTable().rows().data().toArray();
483         // console.log(table_data);
484         jQuery.ajax({
485             url: '/brapi/v2/observations/',
486             method: 'POST',
487             headers: { "Authorization": "Bearer "+jQuery.cookie("sgn_session_id") },
488             data: JSON.stringify(table_data),
489             contentType: "application/json; charset=utf-8",
490             beforeSend: function() {
491                 jQuery('#working_modal').modal('show');
492             },
493             success: function(response) {
494                 // console.log(response);
495                 jQuery('#working_modal').modal('hide');
496                 jQuery('#saveResultModalBody').html('<ul class="list-group"><li class="list-group-item list-group-item-success"><span class="badge"><span class="glyphicon glyphicon-ok"></span></span>Analysis results saved successfully in the database.</li></ul>');
497                 jQuery('#saveResultModal').modal('show');
498             },
499             error: function(response) {
500                 // console.log(response);
501                 jQuery('#working_modal').modal('hide');
502                 jQuery('#saveResultModalBody').html('<ul class="list-group"><li class="list-group-item list-group-item-danger"><span class="badge"><span class="glyphicon glyphicon-remove"></span></span>Error while trying to save the analysis results.</li></ul>');
503                 jQuery('#saveResultModal').modal('show');
504             }
505         });
508     });
512 function _load_image_search_results() {
513     images_table = jQuery('#image_analysis_image_search_results').DataTable({
514         'destroy' : true,
515         'searching' : false,
516         'ordering'  : false,
517         'processing': true,
518         'serverSide': true,
519         'scrollX': true,
520         'lengthMenu': [10,20,50,100,1000,5000],
521         'ajax': { 'url':  '/ajax/search/images',
522             'data': function(d) {
523               d.html_select_box = "image_analysis_select";
524               d.image_description_filename_composite = jQuery('#image_description_filename_composite').val();
525               d.image_person = jQuery('#image_submitter').val();
526               d.image_tag = jQuery('#image_tag').val();
527               d.image_stock_uniquename = jQuery('#image_stock_uniquename').val();
528               d.image_project_name = jQuery('#html_image_analysis_trial_select').val();
529             }
530         }
531     });
534 function _load_analysis_activity_graph(date_range) {
536     d3.json('/ajax/image_analysis/activity', function(error, response) {
538         var activity = JSON.parse(response.activity);
539         // console.log("Activity array is: ");
540         // console.log(activity);
542         // Iterate through each data point and parse date strings into dates
543         var parseTime = d3.timeParse("%Y-%m-%d");
544         var data = [];
546         jQuery.each(activity, function (index, element) {
547             data.push({
548                 'date': parseTime(element.date)
549             })
550         });
552         // Set canvas margins
553         var margin = {top: 20, right: 50, bottom: 30, left: 50};
554         var width = 800 - margin.left - margin.right;
555         var height = 500 - margin.top - margin.bottom;
557         // Create svg object
558         var svg = d3.select('#image_analysis_usage').append('svg')
559             .attr('width', width + margin.left + margin.right)
560             .attr('height', height + margin.top + margin.bottom)
561             .append('g')
562             .attr('transform', `translate(${margin.left}, ${margin.top})`);
564         // Set x (timeseries) and y (linear) scales
565         var xScale = d3.scaleTime().range([0, width]);
566         var yScale = d3.scaleLinear().range([height, 0]);
568        var dayExtent = d3.extent(data, function (d) { return d.date; });
570        console.log("day extent is "+dayExtent);
572        // Create one bin per day, use an offset to include the first and last days
573        var dayBins = d3.timeDays(d3.timeDay.offset(dayExtent[0],-1),
574                                  d3.timeDay.offset(dayExtent[1], 1));
576        var x = d3.scaleTime()
577            .domain(dayExtent)
578            .rangeRound([0, width]);
580        // Scale the range of the data in the y domain
581        var y = d3.scaleLinear()
582                   .range([height, 0]);
584        var xAxis = d3.axisBottom(x)
585                       .tickArguments([d3.timeDay.every(1)])
586                       .tickFormat(d3.timeFormat('%d-%b'))
587                       .ticks(10);
589        // Set the parameters for the histogram
590        var histogram = d3.histogram()
591                           .value(function(d) { return d.date; })
592                           .domain(x.domain())
593                           .thresholds(x.ticks(dayBins.length));
596        // Group the data for the bars
597        var bins = histogram(data);
599        y.domain([0, d3.max(bins, function(d) { return d.length; })]);
601        var hist = svg.selectAll("rect")
602             .data(bins)
603             .enter().append("rect")
604             .attr("class", "bar")
605             .attr("x", 1)
606             .attr("transform", function(d) {
607                return "translate(" + x(d.x0) + "," + y(d.length) + ")";
608             })
609             .attr("width", function(d) {
610                return x(d.x1) - x(d.x0) -1 ;
611             })
612             .attr("height", function(d) {
613                return height - y(d.length);
614             });
616        // Add the x axis
617        svg.append("g")
618             .attr("transform", "translate(0," + height + ")")
619             .call(xAxis)
621        // Add the y axis
622        svg.append("g")
623             // .call(d3.axisLeft(y).ticks(d3.max(bins, function(d) { return d.length; })))
624             .call(d3.axisLeft(y).ticks(15))
625             .append("text")
626             .attr("fill", "#000")
627             .attr("transform", "rotate(-90)")
628             .attr("y", 0 - margin.left)
629             .attr("x",0 - (height / 2))
630             .attr("dy", "1em")
631             .style("text-anchor", "middle")
632             .text("Number of Images Analyzed");
634     });
637 function format ( d ) {
638     var detail_rows = '';
639     d.details.forEach(function (image, index) {
640         var result = image.analyzed_link;
641         var text;
642         if (result.startsWith("Error: ")) {
643             text = result;
644         } else {
645             text = '<img src="'+result+'">';
646         }
647         detail_rows +=
648         `<tr>
649           <td>`+image.image_name+`</td>
650           <td>`+text+`</td>
651           <td>`+image.value+`</td>
652         </tr>`
653     });
655 return `<table class="table">
656             <thead>
657                 <tr>
658                   <th scope="col">Image Name</th>
659                   <th scope="col">Analyzed Image</th>
660                   <th scope="col">Value</th>
661                 </tr>
662              </thead>
663              <tbody>` + detail_rows + `</tbody>
664         </table>`;
668 </script>