Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
3bfc134
Adding Thumbnail_Figure to Publications menu
Mar 29, 2013
990aab4
New base.html for Script UI dialogs. Thumbs script fields
Mar 29, 2013
602c378
Adding Tags to Thumbnail_Figure UI
Mar 29, 2013
059c790
Sort & group thumbs by selected Tags
Mar 31, 2013
a3f7137
Improved layout of tag sorting in table of thumbs
Apr 1, 2013
cc96a74
Only load Tags that are used on thumbnails
Apr 2, 2013
cea4e9a
Support loading thumbs via 'Dataset'
Apr 3, 2013
3c1b862
Removing duplicate Tags
Apr 3, 2013
30ca8b7
Tag sorting within Datasets of thumbs
Apr 3, 2013
b5e770b
Show 'untagged' images checkbox updates UI
Apr 7, 2013
df0ab10
Slider zooms thumbnails
Apr 7, 2013
aed2cc9
Update Max Columns on change
Apr 7, 2013
8b02970
Preserve order of Tag_IDs
Apr 8, 2013
cfca3f3
Fix tiny bug in Max_Columns with <div> tag subsets
Apr 8, 2013
4ff9351
Handle up/down clicks on Max_Columns
Apr 9, 2013
8ee4dea
If no images are Tagged, show message instead of Tag filter
Apr 9, 2013
3a29448
If no Tags Chosen, return to initial layout
Apr 9, 2013
54a04a4
Improving grouping of thumbs under tags
Apr 10, 2013
9c5b9ed
Fix OME.openPopup() to show script. Removed extra jQuery include
Apr 10, 2013
ad5c2b9
Fix table row spacing bug
Apr 10, 2013
764783d
imageUtil.paintThumbnailGrid() handles 'topLabel'. See #10686
Apr 15, 2013
4e00175
Ensure the Chosen plugin input wider than largest Tag
Apr 15, 2013
0b6d1cf
Fix hiding of single subset with multi-tags. PR #1054
Apr 18, 2013
43ff8d2
Use Dataset name for Thumb Figure name
Apr 22, 2013
f05df84
Attach Thumbnail_Figure to 1st Dataset
May 9, 2013
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 26 additions & 10 deletions components/tools/OmeroPy/src/omero/util/imageUtil.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ def getThumbnailSet(thumbnailStore, length, pixelIds):
return None

def paintThumbnailGrid(thumbnailStore, length, spacing, pixelIds, colCount, bg=(255,255,255),
leftLabel=None, textColour=(0,0,0), fontsize=None):
leftLabel=None, textColour=(0,0,0), fontsize=None, topLabel=None):
"""
Retrieves thumbnails for each pixelId, and places them in a grid, with White background.
Option to add a vertical label to the left of the canvas
Expand All @@ -144,22 +144,28 @@ def paintThumbnailGrid(thumbnailStore, length, spacing, pixelIds, colCount, bg=(
while (colCount * rowCount) < imgCount: # check that we have enough rows and cols...
rowCount += 1

leftSpace = spacing
leftSpace = topSpace = spacing
minWidth = 0

textHeight = 0
if leftLabel:
if leftLabel or topLabel:
# if no images (no rows), need to make at least one row to show label
if rowCount == 0: rowCount = 1
if leftLabel is not None and rowCount == 0: rowCount = 1
if fontsize == None:
fontsize = length/10 + 5
font = getFont(fontsize)
textHeight = font.getsize("Textq")[1]
leftSpace = spacing + textHeight + spacing

if leftLabel:
textWidth, textHeight = font.getsize(leftLabel)
leftSpace = spacing + textHeight + spacing
if topLabel:
textWidth, textHeight = font.getsize(topLabel)
topSpace = spacing + textHeight + spacing
minWidth = leftSpace + textWidth + spacing

# work out the canvas size needed, and create a white canvas
colsNeeded = min(colCount, imgCount)
canvasWidth = leftSpace + colsNeeded * (length+spacing)
canvasHeight = rowCount * (length+spacing) + spacing
canvasWidth = max(minWidth, (leftSpace + colsNeeded * (length+spacing)))
canvasHeight = topSpace + rowCount * (length+spacing) + spacing
mode = "RGB"
size = (canvasWidth, canvasHeight)
canvas = Image.new(mode, size, bg)
Expand All @@ -177,6 +183,16 @@ def paintThumbnailGrid(thumbnailStore, length, spacing, pixelIds, colCount, bg=(
verticalCanvas = textCanvas.rotate(90)
pasteImage(verticalCanvas, canvas, 0, 0)
del draw

if topLabel is not None:
labelCanvasWidth = canvasWidth
labelCanvasHeight = textHeight + spacing
labelSize = (labelCanvasWidth, labelCanvasHeight)
textCanvas = Image.new(mode, labelSize, bg)
draw = ImageDraw.Draw(textCanvas)
draw.text((spacing, spacing), topLabel, font=font, fill=textColour)
pasteImage(textCanvas, canvas, leftSpace, 0)
del draw

# loop through the images, getting a thumbnail and placing it on a new row and column
r = 0
Expand All @@ -189,7 +205,7 @@ def paintThumbnailGrid(thumbnailStore, length, spacing, pixelIds, colCount, bg=(
thumbImage = Image.open(StringIO.StringIO(thumbnail)) # make an "Image" from the string-encoded thumbnail
# paste the image onto the canvas at the correct coordinates for the current row and column
x = c*(length+spacing) + leftSpace
y = r*(length+spacing) + spacing
y = r*(length+spacing) + topSpace
pasteImage(thumbImage, canvas, x, y)

# increment the column, and if we're at the last column, start a new row
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,16 @@ def listFigureScripts(self, objDict=None):
if i.getSizeC() > 1:
splitView['enabled'] = True
break
thumbnailFig = {'id': 'Thumbnail', 'name': 'Thumbnail Figure', 'enabled': False,
'tooltip': "Export a figure of thumbnails, optionally sorted by tag"}
# Thumbnail figure is enabled if we have Datasets or Images selected
if self.image or self.dataset:
thumbnailFig['enabled'] = True
elif objDict is not None:
if 'image' in objDict or 'dataset' in objDict:
thumbnailFig['enabled'] = True
figureScripts.append(splitView)
figureScripts.append(thumbnailFig)
return figureScripts


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,6 @@
//

$(document).ready(function() {
OME.setupAjaxError("{% url fsend %}"); // just in case!

$('#script_form').ajaxForm({
success: function(data) {
window.opener.OME.showActivities();
self.close();
}
});


// Basically what we're doing here is supplementing the form fields
Expand Down Expand Up @@ -307,7 +299,4 @@ $(document).ready(function() {
setImgSize(ui.value);
}
});

// For 'number' fields, only allow numbers input.
$(".number").numbersOnly();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
//
// Copyright (C) 2013 University of Dundee & Open Microscopy Environment.
// All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//

$(document).ready(function() {

// Clear selection (only needed on page 'refresh')
$("#tagChooser option").removeAttr('selected');


// Show / Hide un-tagged images after sorting or checkbox click
var updateNotTagged = function() {
var show_untagged_images = $("input[name=Show_Untagged_Images]").is(":checked");
if (show_untagged_images) {
$(".notTagged").show();
} else {
$(".notTagged").hide();
}
};
$("input[name=Show_Untagged_Images]").click(updateNotTagged);


// Add <br> to break up long rows of thumbnails
var updateRowCount = function(){
var colCount = parseInt($("input[name=Max_Columns]").val(), 10);
$(".thumbnail_set td:has(.img_panel)").each(function(){
var $td = $(this);
$("br", $td).remove(); // remove old <br>
// $(":nth-child(" + colCount + "n)", $td).after("<br/>");
// <td> may have <div> children that break up child thumbs
var i = 1;
$td.children().each(function(){
var $child = $(this);
if (i % colCount === 0) {
$child.after("<br/>")
}
if (this.nodeName.toLowerCase() == 'div') {
i = 1; // new row
} else {
i++;
}
});
});
};
$("input[name=Max_Columns]").bind('keyup input', updateRowCount);

updateRowCount(); // initialise layout


// If no tags are chosen, we need to return to initial state
var clearTagSorting = function() {
$(".thumbnail_set").each(function() {
var $this = $(this),
$toRemove = $('tr:has(.img_panel)', $this);

$tr = $('<tr><td></td></tr>');
$tr.appendTo($this);
$td = $('<td></td>').appendTo($tr);
$(".img_panel", $this).appendTo($td);
// now that we've moved the images, we can clean up!
$toRemove.remove();
updateRowCount();
});
}


// Initialise the Chosen plugin, handle Adding / Removing of Tags...
var $TagIdsInput = $("input[name=Tag_IDs]");
var selectedTagIds = [];
$("#tagChooser")
.chosen({placeholder_text:'Choose Tags'})
.change(function(evt, data) {
if (data.deselected) {
var toRemove = data.deselected;
selectedTagIds.splice(selectedTagIds.indexOf(toRemove), 1);
} else if (data.selected) {
selectedTagIds.push(data.selected);
}
// update input for form submission
$TagIdsInput.val(selectedTagIds.join(","));

if (selectedTagIds.length == 0) {
clearTagSorting();
return;
}

var tagValues = {};
// Have to look-up the Tag names from the UI
$("#tagChooser option:selected").each(function(){
var $this = $(this);
tagValues[$this.attr('value')] = $this.text();
});

// Now we need to sort images by ids....
// Let's assign letters to tags in order, so we can use to sort,
// E.g. 'Metaphase' == 'a', 'Anaphase' == 'b', 'Dead' == 'c'
// Then we can combine: 'Metaphase'+'Dead' ==> 'ac', 'Anaphase'+'Dead' ==> 'bc'
// And sort: 'a', 'ab', 'abc', 'b', 'bc', 'c', '' (untagged)
var letters = "abcdefghijklmnopqrstuvwxyz",
tagLetters = {},
letterTags = {}; // backwards map to decode
for (var t=0; t<selectedTagIds.length; t++){
tagLetters[selectedTagIds[t]] = letters[t];
letterTags[letters[t]] = selectedTagIds[t];
}

// E.g. 'ac' ==> ['Metaphase', 'Dead']
var getTagsFromKey = function(key) {
var rv = [];
for (var k=0; k<key.length; k++) {
var tagId = letterTags[key[k]];
rv.push(tagValues[tagId]);
}
return rv;
}

// For each 'Dataset' (or set of Images)...
$(".thumbnail_set").each(function() {
// Make a data structure of image-ids : [tag ids]
var $this = $(this),
imageTags = {},
imgIds = [];
$(".img_panel", $this).each(function(){
var $img = $(this),
iId = $img.attr('data-iId'),
tagString = $img.attr('data-tags');
imageTags[iId] = tagString.split(",");
imgIds.push(iId);
});

// Prepare for sorting...
sortedImgs = []
for (var i=0; i<imgIds.length; i++) {
// Build up a string based on the image's Tags, that can be used to sort
var imgId = imgIds[i],
tagIds = imageTags[imgId],
letterKeys = [];
for (var c=0; c<selectedTagIds.length; c++) {
var tid = selectedTagIds[c];
if (tagIds.indexOf(tid) > -1) {
letterKeys.push(tagLetters[tid]);
}
}
var tagKey = letterKeys.join(""); // E.g. 'a' or 'bc'
sortedImgs.push({'id':imgId, 'tagKey':tagKey});
}
// Do the Sorting!
sortedImgs.sort(function(a, b) {
var x = a['tagKey'], y = b['tagKey'];
if (x.length === 0) return 1;
if (y.length === 0) return -1;
return ((x < y) ? -1 : ((x > y) ? 1 : 0));
});

// Group Images with same tags. E.g. all of the with key 'ac'
var results = [], // List of {'tagNames':[], 'imgIds':[]}
rkey = "", rIds = [];
for (var s=0; s<sortedImgs.length; s++) {
var sImg = sortedImgs[s];
if (sImg.tagKey == rkey) {
rIds.push(sImg.id);
} else {
if (rIds.length > 0) {
results.push({'rkey':rkey, 'tags': getTagsFromKey(rkey), 'imgIds':rIds});
}
rIds = [sImg.id];
rkey = sImg.tagKey;
}
}
results.push({'rkey':rkey, 'tags': getTagsFromKey(rkey), 'imgIds':rIds});


// Update the UI
var $toRemove = $('tr:has(.img_panel)', $this);

// For each Tag combination... (E.g. 'Metaphase'+'Dead')
var topLevelTag = "";
var $td, $tr;
for (var r=0; r<results.length; r++) {

var tagData = results[r],
tagsText = tagData.tags.join(", "); // 'Metaphase, Dead'
if (tagsText.length === 0) {
tagsText = "Not Tagged";
}
if (tagsText.length === 0 || tagData.tags[0] !== topLevelTag) {
// start a new container...
topLevelTag = tagData.tags[0] || tagsText; // Group under 1st tag (unless we have no tags)

$tr = $('<tr><th><h2>' + topLevelTag + '</h2></th></tr>');
$tr.appendTo($this);
$td = $('<td></td>').appendTo($tr);
}
$td.append('<div class="subsetLabel">'+ tagsText + '</div>');

if (tagsText === "Not Tagged") {
$tr.addClass('notTagged');
}

// Add the images (move from previous position)...
for (var i=0; i<tagData.imgIds.length; i++) {
$('#thumbnail-'+tagData.imgIds[i]).appendTo($td);
}

}
// now that we've moved the images, we can clean up!
$toRemove.remove();

// tidy up by hiding subset labels where there is only 1 subset
// AND the subset has only 1 Tag
$('tr:has(.img_panel)', $this).each(function(){
var $subsetLabels = $(this).find('.subsetLabel');
if (($subsetLabels.length === 1)
&& ($subsetLabels.text().split(", ").length === 1)) {
$subsetLabels.hide();
}
});

updateNotTagged();
updateRowCount();
});

});

// Hacks to make the Chosen plugin to wide enough.
$(".chzn-container").width('350px');
$(".chzn-container input").width('300px');

// Bonus feature - Zoom the preview thumbs with slider
// Make a list of styles (for quick access on zoom)
var img_panel_styles = [];
$(".img_panel").each(function(){
img_panel_styles.push(this.style);
});
var setImgSize = function(size) {
console.log(size);
var i, l = img_panel_styles.length;
for (i=0; i<l; i++) {
img_panel_styles[i].maxWidth = size + "px";
img_panel_styles[i].maxHeight = size + "px";
}
};
$("#img_size_slider").slider({
max: 96,
min: 20,
value: 50,
slide: function(event, ui) {
setImgSize(ui.value);
}
});

});
Loading