Thursday, March 27, 2008

Dissecting Hard Rock Memorabilia and Silverlight Deep Zoom - Part 2

In Part 1 of this series, I discussed the logic to arrange the images in a grid - where the height of all images in a row is the same but can change across rows. The objective is to maintain the aspect ratio of the image and to not stretch it. This post provides some sample code snippets that implement the algorithm.

Some considerations
  1. The code caters to spacing between images even thought this was not discussed in the logic
  2. The _imagesToShow and _imagesToHide collections should be populated with MutliScaleSubImages prior to calling ArrangeImages
  3. There is 1 condition that is not addressed: if the height of all rows exceed the height of the container - it can be ignored for now if clipping is acceptable
  4. This code is based off the sample code published by Scott Hanselman and hence the use of the variable msi (which is an instance of MultiScaleImage)
  5. I am sure this code and/or logic can be improved upon so please feel free to leave your comments


List<MultiScaleSubImage> _imagesToShow = new List<MultiScaleSubImage>();
List<MultiScaleSubImage> _imagesToHide = new List<MultiScaleSubImage>();

public void ArrangeImages()
{
double containerAspectRatio = msi.Width / msi.Height;
double spaceBetweenImages = 0.005;

List<SubImage> subImages = new List<SubImage>();
_imagesToShow.ForEach(subImage => subImages.Add(new SubImage(subImage)));

// Capture the total width of all images
double totalImagesWidth = 0.0;
subImages.ForEach(subImage => totalImagesWidth += subImage.Width);

// Calculate the total number of rows required to display all the images
int numRows = (int)Math.Sqrt((totalImagesWidth / containerAspectRatio)+1);

// Create the rows
List<Row> rows = new List<Row>(numRows);
for (int i=0; i<numRows; i++)
rows.Add(new Row(spaceBetweenImages));

double widthPerRow = totalImagesWidth / numRows;
double imagesWidth = 0;
// Separate the images into rows. The total width of all images in a row should not exceed widthPerRow
for (int i=0, j=0; i<numRows; i++, imagesWidth=0) {
while (imagesWidth < widthPerRow && j < subImages.Count) {
rows[i].AddImage(subImages[j]);
subImages[j].RowNum = i;
imagesWidth += subImages[j++].Width;
}
}

// At this point in time the subimage height is 1
// If we assume that the total height is also 1 we need to scale the subimages to fit within a total height of 1
// If the total height is 1, the total width is aspectRatio. Hence (aspectRatio)/(total width of all images in a row) is the scaling factor.
// Added later: take into account spacing between images
rows.ForEach(Row => Row.Scale(containerAspectRatio));

// Calculate the total height, with space between images, of the images across all rows
// Also adjust the colNum for each image
double totalImagesHeight = -spaceBetweenImages(numRows - 1) * spaceBetweenImages;
rows.ForEach(Row => totalImagesHeight += Row.Height + spaceBetweenImages);
Debug.Assert(totalImagesHeight <= 1.0); // TODO: Handle this condition

// The totalImagesHeight should not exceed 1.
// if it does, we need to scale all images by a factor of (1 / totalImagesHeight)
// This takes care of consideration #3 in the text above
if (totalImagesHeight > 1) {
subImages.ForEach(subImage => subImage.Scale(1 / (totalImagesHeight+spaceBetweenImages)));
totalImagesHeight = (numRows - 1) * spaceBetweenImages;
rows.ForEach(Row => totalImagesHeight += Row.Height);
Debug.Assert(totalImagesHeight <= 1);
}

// Calculate the top and bottom margin
double margin = (1 - totalImagesHeight) / 2;

// First hide all the images that should not be displayed
_imagesToHide.ForEach(subImage => subImage.Opacity = 0.0);

// Then display the displayable images to scale
for (int i=0; i<_imagesToShow.Count; i++) {
_imagesToShow[i].Opacity = 1.0; // in case this was hidden earlier

double X = rows[subImages[i].RowNum].CalcX(subImages[i].ColNum);
double Y = margin + ((subImages[i].RowNum == 0) ? 0 : rows[subImages[i].RowNum-1].Height + spaceBetweenImages);
for (int j=0; j<subImages[i].RowNum; j++)
Y += spaceBetweenImages + rows[j].Height;

_imagesToShow[i].ViewportWidth = containerAspectRatio/ subImages[i].Width;
// AnimateImage(_imagesToShow[i], new Point(-(X / subImages[i].Width), -(Y / subImages[i].Width))); // for animation, use this statement instead of the next one
_imagesToShow[i].ViewportOrigin = new Point(-(X / subImages[i].Width), -(Y / subImages[i].Width));
}
}

The helper classes:

public class SubImage
{
private Size _imageSize = new Size(0.0, 1.0); //normalized height is 1 for all images
private int _colNum = 0;
private int _rowNum = 0;

public SubImage(MultiScaleSubImage image)
{
// Capture the normalized size of each image (fixed height of 1)
// This normalization is required since we want the height of all images to be the same but the widths can differ
_imageSize.Width = image.AspectRatio;
}

public int ColNum
{
get { return _colNum; }
set { _colNum = value; }
}

public int RowNum
{
get { return _rowNum; }
set { _rowNum = value; }
}

public double Width { get { return _imageSize.Width; } }
public double Height { get { return _imageSize.Height; } }

public void Scale(double scaleBy)
{
_imageSize.Width *= scaleBy;
_imageSize.Height *= scaleBy;
}
}

public class Row
{
List<SubImage> _subImages = new List<SubImage>();
double _spaceBetweenImages = 0;

public Row(double spaceBetweenImage)
{
_spaceBetweenImages = spaceBetweenImage;
}

public double TotalWidth
{
get
{
double totalWidth = 0;
_subImages.ForEach(image => totalWidth += image.Width);
return totalWidth;
}
}

public int ImageCount {get{return _subImages.Count;}}
public double Height {get{return (ImageCount <= 0) ? 0 : _subImages[0].Height;}}
public double TotalSpaceBetweenImages {get{return (ImageCount <= 0) ? 0 : (ImageCount-1) * _spaceBetweenImages;}}

public void AddImage(SubImage subImage)
{
_subImages.Add(subImage);
subImage.ColNum = _subImages.Count - 1;
}

public void Scale(double canvasWidth)
{
double scaleBy = (canvasWidth - TotalSpaceBetweenImages) / TotalWidth;
foreach (SubImage subImage in _subImages)
subImage.Scale(scaleBy);
}

public double CalcX(int colNum)
{
Debug.Assert(colNum < _subImages.Count);

double X = 0;
for (int i=0; i<colNum; i++) {
X += _subImages[i].Width + _spaceBetweenImages;
}
return X;
}
}

4 comments:

Anonymous said...

Hi! Loving your stuff, I'm trying to learn as much as I can about Silverlight and your blog has been a great reference.

I do have a question for you though. Is the code you have for ArrageImages() complete? If so, I must be missing something as the sub doesn't do anything when fired.

Any ideas? I'd love to implement your algorithm but at this point I'm stuck.

Wilfred Pinto said...

Anonymous,

It should be complete! I can send you the entire source code if that would help. Post a comment with your email address and I will send you the code. I will not publish the post so don't worry about your email making it to this blog.

Unknown said...

Hi Wilfred,

Is it possible to get a working example of your code? I am playing around with silverlight 2.0 and Deep Zoom - all examples really help.

Thanks

Neil

Wilfred Pinto said...

Neil,

The link to the source code is available in this blog. Just navigate to the home page, projectsilverlight.blogspot.com, and search for the source code there.

Wilfred