Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e25529a
Adding Thumbnail_Figure to Publications menu
Mar 29, 2013
b34e0d2
New base.html for Script UI dialogs. Thumbs script fields
Mar 29, 2013
b1333b7
Adding Tags to Thumbnail_Figure UI
Mar 29, 2013
92b73ed
Sort & group thumbs by selected Tags
Mar 31, 2013
3a3ee6e
Improved layout of tag sorting in table of thumbs
Apr 1, 2013
4784df7
Only load Tags that are used on thumbnails
Apr 2, 2013
ac2dc9e
Support loading thumbs via 'Dataset'
Apr 3, 2013
46e4412
Removing duplicate Tags
Apr 3, 2013
23a3458
Tag sorting within Datasets of thumbs
Apr 3, 2013
bb2d680
Show 'untagged' images checkbox updates UI
Apr 7, 2013
378b968
Slider zooms thumbnails
Apr 7, 2013
5d0ae37
Update Max Columns on change
Apr 7, 2013
8479261
Preserve order of Tag_IDs
Apr 8, 2013
e114a23
Fix tiny bug in Max_Columns with <div> tag subsets
Apr 8, 2013
861f57f
Handle up/down clicks on Max_Columns
Apr 9, 2013
e5cdc6a
If no images are Tagged, show message instead of Tag filter
Apr 9, 2013
ef4e186
If no Tags Chosen, return to initial layout
Apr 9, 2013
9ea1c8e
Improving grouping of thumbs under tags
Apr 10, 2013
2d831f5
Fix OME.openPopup() to show script. Removed extra jQuery include
Apr 10, 2013
56b9263
Fix table row spacing bug
Apr 10, 2013
8975446
imageUtil.paintThumbnailGrid() handles 'topLabel'. See #10686
Apr 15, 2013
e5862d9
Ensure the Chosen plugin input wider than largest Tag
Apr 15, 2013
f32ccbd
Fix hiding of single subset with multi-tags. PR #1054
Apr 18, 2013
1dc3e32
Use Dataset name for Thumb Figure name
Apr 22, 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