#region gnu_license
/*
Crystal Controls - C# control library containing the following tools:
CrystalControl - base class
CrystalGradientControl - a control that can either have a gradient background or be totally transparent.
CrystalLabel - a homegrown label that can have a gradient or transparent background.
CrystalPanel - a panel that can have a gradient or transparent background.
CrystalTrackBar - a homegrown trackbar that can have a gradient or transparent background.
CrystalToolStripTrackBar - a host for CrystalTrackBar that allows it to work in a ToolStrip.
CrystalImageGridView - a control that hosts thumbnail images in a virtual grid.
CrystalImageGridModel - a data model that holds a collection of CrystalImageItems
to feed to CrystalImageGridView.
CrystalImageItem - a class that describes an Image file.
CrystalThumbnailer - provides thumbnailing methods for images.
CrystalCollector - a base class for a controller that links
CrystalImageGridView to the CrystalImageGridModel.
CrystalFileCollector - a controller that works on disk-based Image files.
CrystalDesignCollector - a controller that works in Visual Studio toolbox designer.
CrystalMemoryCollector - a controller that can be used to add images from memory.
CrystalMemoryZipCollector - a controller that accesses images in zip files by streaming them into memory.
CrystalZipCollector - a controller that accesses images in zip files by unpacking them.
CrystalRarCollector - a controller that accesses images in rar files by unpacking them.
CrystalPictureBox - a picture box control, derived from CrystalGradientControl.
CrystalPictureShow - a control for viewing images and processing slideshows.
CrystalComicShow - a control for viewing comic-book images in the CDisplay format.
Copyright (C) 2006, 2008 Richard Guion
Attilan Software Factory: http://www.attilan.com
Contact: richard@attilan.com
Version 1.0.0
This is a work in progress: USE AT YOUR OWN RISK! Interfaces/Methods may change!
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library 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
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#endregion
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.IO;
using System.Windows.Forms;
using Attilan.Crystal.Tools;
using Attilan.Tools;
using Phydeaux.Utilities;
namespace Attilan.Crystal.Controls
{
/// <summary>
/// Sort types for Crysal Collector
/// </summary>
public enum CrystalSortType
{
/// <summary>
/// Sort by the Image Name.
/// </summary>
ImageName = 0,
/// <summary>
/// Sort by the Image file type
/// </summary>
ImageType = 1,
/// <summary>
/// Sort by the Image creation time
/// </summary>
CreationTime = 2,
/// <summary>
/// Sort by the last modification time
/// </summary>
LastWriteTime = 3,
/// <summary>
/// Sort by the display name
/// </summary>
DisplayName = 4
}
/// <summary>
/// Filter types for Crystal Collector
/// </summary>
public enum CrystalFilterType
{
/// <summary>
/// Display all images
/// </summary>
AllImages = 0,
/// <summary>
/// Display only modified images
/// </summary>
ModifiedImages = 1,
/// <summary>
/// Use customized filter for images
/// </summary>
CustomFilter = 2
}
/// <summary>
/// Event arguments class for UpdateThumbnail event.
/// </summary>
public class CrystalImageUpdateEventArgs : EventArgs
{
/// <summary>
/// The CrystalImageItem information about the image with the updated thumbnail.
/// </summary>
public CrystalImageItem updatedImage = null;
/// <summary>
/// Index of the updated image in the model.
/// </summary>
public int imageIndex = -1;
/// <summary>
/// Total numbers of images in the model where the image is located.
/// </summary>
public int imageCount = -1;
/// <summary>
/// Constructor that takes a CrystalImageItem object.
/// </summary>
/// <param name="theImage">The CrystalImageItem object that was updated.</param>
public CrystalImageUpdateEventArgs(CrystalImageItem theImage)
{
updatedImage = theImage;
}
}
/// <summary>
/// The base collector object which supports the base properties and methods for Image collection.
/// </summary>
[ToolboxItem(false)]
abstract public class CrystalCollector : IDisposable
{
#region Fields
/// <summary>
/// This event lets callers know when a CrystalImageItem has a new thumbnail available.
/// </summary>
public event EventHandler ImageUpdated;
/// <summary>
/// This event lets callers know when the background thread has completed
/// thumbnailing all images.
/// </summary>
public event EventHandler ThumbnailComplete;
/// <summary>
/// This event lets callers know when the virtual grid has been resized.
/// </summary>
public event EventHandler VirtualGridUpdated;
/// <summary>
/// The CrystalImageGridModel that is a collection of CrystalImageItem objects.
/// </summary>
protected CrystalImageGridModel _gridModel = null;
private string _imageLocation;
private string _imageFilter;
private bool _persistThumbs = true;
private ICrystalThumbnailer _thumbnailer = null;
/// <summary>
/// Determines whether the background thumbnail image thread is running.
/// </summary>
protected bool _thumbnailThreadRunning = false;
#endregion
#region ICrystalCollector Property Implementation
/// <summary>
/// The model that represents a virtual grid of CrystalImageItems.
/// </summary>
public virtual CrystalImageGridModel GridModel
{
get
{
if (_gridModel == null)
{
_gridModel = new CrystalImageGridModel();
}
return _gridModel;
}
}
/// <summary>
/// Returns the grid view attached to the model.
/// </summary>
public CrystalImageGridView GridView
{
get
{
if (_gridModel == null)
{
return null;
}
else
{
return _gridModel.View;
}
}
}
/// <summary>
/// The folder location where images will be located.
/// </summary>
public string ImageLocation
{
get { return _imageLocation; }
set { _imageLocation = value; }
}
/// <summary>
/// String containing a series of extensions, separated by commas ("*.bmp,*.jpg").
/// </summary>
public string ImageFilter
{
get { return _imageFilter; }
set { _imageFilter = value; }
}
/// <summary>
/// If true, thumbnails are persisted to storage.
/// </summary>
public bool PersistThumbnails
{
get { return _persistThumbs; }
set { _persistThumbs = value; }
}
/// <summary>
/// The object that processes thumbnail images.
/// </summary>
public ICrystalThumbnailer Thumbnailer
{
get { return _thumbnailer; }
set { _thumbnailer = value; }
}
/// <summary>
/// Determines whether the background thumbnail image thread is running.
/// </summary>
public bool ThumbnailThreadRunning
{
get { return _thumbnailThreadRunning; }
}
#endregion
#region Collector Methods
/// <summary>
/// Creates a CrystalImageItem object.
/// </summary>
/// <returns>A CrystalImageItem object.</returns>
public virtual CrystalImageItem CreateCrystalImage()
{
return new CrystalImageItem();
}
/// <summary>
/// Creates a CrystalGroupItem object.
/// </summary>
/// <returns>A CrystalGroupItem object.</returns>
public virtual CrystalGroupItem CreateGroupItem()
{
return new CrystalGroupItem();
}
/// <summary>
/// Loads the full image and places it into the CrystalImageItem object.
/// </summary>
/// <param name="imageItem">The CrystalImageItem object with the Image information.</param>
/// <returns>True if successful, false if an error occurred.</returns>
abstract public bool LoadImage(ref CrystalImageItem imageItem);
/// <summary>
/// Loads the full image and places it into the CrystalImageItem object.
/// </summary>
/// <param name="ImageFullPath">The location of the image.</param>
/// <returns>Image if successful, null if an error occurred.</returns>
abstract public Image LoadImage(string ImageFullPath);
/// <summary>
/// Loads the thumbnail image and places it into the CrystalImageItem object.
/// </summary>
/// <param name="imageItem">The CrystalImageItem object with the Image information.</param>
/// <returns>True if successful, false if an error occurred.</returns>
abstract public bool LoadThumbnailImage(ref CrystalImageItem imageItem);
/// <summary>
/// Collects a thumbnail for a single image item.
/// </summary>
/// <param name="crystalImage">Image item from which a thumbnail image will be collected.</param>
/// <param name="overwriteExisting">True means regenerate the thumbnail if one already exists.</param>
/// <returns>True if a thumbnail was generated, false otherwise.</returns>
public abstract bool CollectThumbnailImage(ref CrystalImageItem crystalImage, bool overwriteExisting);
/// <summary>
/// Collects the images at the ImageLocation into the model and then tells the view to display them.
/// </summary>
/// <returns>True if images collected, false otherwise.</returns>
abstract public bool CollectImages();
/// <summary>
/// Collects the images at the group item's location into the model and then tells the view to display them.
/// </summary>
/// <param name="groupItem">CrystalGroupItem containing location to collect images from.</param>
/// <returns>True if images collected, false otherwise.</returns>
abstract public bool CollectImages(CrystalGroupItem groupItem);
/// <summary>
/// Stops the image collection operation. Kills any background threads operating on images.
/// </summary>
abstract public void StopCollection();
/// <summary>
/// Purges the collection of images that belongs to this object.
/// </summary>
abstract public void PurgeImages();
/// <summary>
/// Call this to let all subscribers know of image update.
/// </summary>
/// <param name="crystalImage">CrystalImageItem with information about the image that changed.</param>
protected virtual void UpdateSubscribers(CrystalImageItem crystalImage)
{
if (ImageUpdated != null)
{
CrystalImageUpdateEventArgs eventArgs = new CrystalImageUpdateEventArgs(crystalImage);
EventNotifier.UpdateSubscribers(ImageUpdated, eventArgs, this);
}
}
/// <summary>
/// Call this to let all subscribers know of image update.
/// </summary>
/// <param name="crystalImage">CrystalImageItem with information about the image that changed.</param>
/// <param name="imageIndex">Model index of the image that was updated.</param>
protected virtual void UpdateSubscribers(CrystalImageItem crystalImage, int imageIndex)
{
if (ImageUpdated != null)
{
CrystalImageUpdateEventArgs eventArgs = new CrystalImageUpdateEventArgs(crystalImage);
eventArgs.imageIndex = imageIndex;
if (GridModel != null)
eventArgs.imageCount = GridModel.Count;
EventNotifier.UpdateSubscribers(ImageUpdated, eventArgs, this);
}
}
/// <summary>
/// Call this to let subscribers know that thumbnailing is complete.
/// </summary>
protected virtual void UpdateThumbnailComplete()
{
CrystalLogger.LogEvent("Thumbnail thread has finished");
if (ThumbnailComplete != null)
{
EventArgs args = new EventArgs();
EventNotifier.UpdateSubscribers(ThumbnailComplete, args, this);
}
}
/// <summary>
/// Call this to let subscribers know that the virtual grid has been resized.
/// </summary>
protected virtual void UpdateResizeVirtualGrid()
{
if (VirtualGridUpdated != null)
{
EventArgs args = new EventArgs();
EventNotifier.UpdateSubscribers(VirtualGridUpdated, args, this);
}
}
/// <summary>
/// Adds an object derived from CrystalImageGridView to this controller.
/// </summary>
/// <param name="imageGridView">The CrystalImageGridView object that this controller needs to call.</param>
public virtual void SetupView(CrystalImageGridView imageGridView)
{
GridModel.View = imageGridView;
imageGridView.GridController = this;
}
/// <summary>
/// Applies a color to a CrystalImageItem objects in the model.
/// </summary>
/// <param name="borderColor">Border color.</param>
public virtual void ApplyBorderColor(Color borderColor)
{
if (GridModel == null)
return;
foreach (CrystalImageItem imageItem in GridModel)
{
imageItem.BorderColor = borderColor;
}
}
/// <summary>
/// Adds the image to the model.
/// </summary>
/// <param name="fullImage">Full size image.</param>
/// <param name="thumbImage">Thumbnailed image.</param>
/// <param name="imageName">Image name.</param>
/// <returns>Returns the CrystalImageItem that was added to the model.</returns>
public virtual CrystalImageItem AddImage(Image fullImage, Image thumbImage, string imageName)
{
if ((fullImage == null) || (thumbImage == null))
return null;
CrystalImageItem imageItem = CreateCrystalImage();
imageItem.FullImage = fullImage;
imageItem.ThumbnailImage = thumbImage;
imageItem.ImageName = imageName;
imageItem.DisplayName = Path.GetFileNameWithoutExtension(imageName);
imageItem.ToolTipText = imageName;
AddImageItem(imageItem);
return imageItem;
}
/// <summary>
/// Adds the CrystalImageItem object to the model.
/// </summary>
/// <param name="imageItem">CrystalImageItem object.</param>
public virtual void AddImageItem(CrystalImageItem imageItem)
{
if (GridModel == null)
_gridModel = new CrystalImageGridModel();
_gridModel.AddItem(imageItem);
}
/// <summary>
/// Updates the image grid within the model and adjusts the view.
/// </summary>
public void UpdateGrid()
{
if ((GridModel != null) && (GridModel.View != null))
{
// Have the model calculate its virtual size,
// and the size/position of each image item.
_gridModel.CalculateVirtualGrid();
// Tell the view to start drawing the image grid
UpdateResizeVirtualGrid();
}
}
/// <summary>
/// Replaces the current grid model.
/// </summary>
/// <param name="newGridModel">New grid model to replace in current collector.</param>
public void ReplaceGrid(CrystalImageGridModel newGridModel)
{
if (newGridModel != null)
{
_gridModel = newGridModel;
UpdateGrid();
}
}
/// <summary>
/// Adds a new image item to the collector, non-threaded.
/// Call this after the collector has been loaded to add new image items.
/// </summary>
/// <param name="imageFileName"></param>
/// <returns>true if image added to model, false otherwise.</returns>
public virtual bool AddNewModelImage(string imageFileName)
{
if (string.IsNullOrEmpty(imageFileName))
return false;
if (File.Exists(imageFileName))
{
/////////////////////////////////////////////////
/// Get the info about the image
FileInfo imageFileInfo = new FileInfo(imageFileName);
/////////////////////////////////////////////////
/// Create the image item
CrystalImageItem theImage = CreateCrystalImage();
theImage.SplitImagePath(imageFileInfo.FullName);
// Default border color is what was set in the view.
if (GridView != null)
theImage.BorderColor = GridView.CellBorderColor;
Thumbnailer.AdjustThumbLocation(theImage);
// Add the image item to the model
AddImageItem(theImage);
// Have the model calculate its virtual size,
// and the size/position of each image item.
GridModel.CalculateVirtualGrid();
//////////////////////////////////
// See if we can load the image
Image fullImage;
if (LoadImage(ref theImage))
{
fullImage = theImage.FullImage;
}
else
return false;
string thumbFullPath = Thumbnailer.GetThumbFullPath(theImage);
bool thumbExists = (Thumbnailer.Exists(thumbFullPath, theImage.LastWriteTime));
//////////////////////////////////
// Create the thumbnail
if (!thumbExists)
{
Image thumbImage = Thumbnailer.Generate(fullImage, theImage.ImageRect.Size);
if (thumbImage != null)
{
if (PersistThumbnails)
{
Thumbnailer.Store(thumbFullPath, thumbImage,
theImage.LastWriteTime, fullImage.RawFormat);
}
}
}
if (fullImage != null)
{
theImage.FullImage = null;
fullImage.Dispose();
}
// Tell the view to start drawing the image grid
UpdateResizeVirtualGrid();
return true;
}
return false;
}
#endregion
#region Sorting methods
/// <summary>
/// Sorts the Crystal list according to the public property in CrystalImageItem.
/// </summary>
/// <param name="orderBy">String containing public property name in CrystalImageItem.</param>
/// <param name="ascendingOrder">If true, sort by ascending order.</param>
protected virtual void SortCrystalList(string orderBy, bool ascendingOrder, List<CrystalImageItem> crystalList)
{
if ((_gridModel == null) || (crystalList == null))
return;
if (ascendingOrder)
orderBy += " ASC";
else
orderBy += " DESC";
// Create the comparer for Person types and define the sort fields.
DynamicComparer<CrystalImageItem> comparer = new DynamicComparer<CrystalImageItem>(orderBy);
// Sort the list.
crystalList.Sort(comparer.Compare);
}
/// <summary>
/// Sorts the Crystal list according to the public property in CrystalImageItem.
/// </summary>
/// <param name="sortType">Sort type to use.</param>
/// <param name="ascendingOrder">If true, sort by ascending order.</param>
protected virtual void SortCrystalList(CrystalSortType sortType, bool ascendingOrder, List<CrystalImageItem> crystalList)
{
string orderBy = string.Empty;
switch (sortType)
{
case CrystalSortType.ImageName:
orderBy = "ImageName";
break;
case CrystalSortType.ImageType:
orderBy = "ImageFormatString";
break;
case CrystalSortType.CreationTime:
orderBy = "CreationTime";
break;
case CrystalSortType.LastWriteTime:
orderBy = "LastWriteTime";
break;
case CrystalSortType.DisplayName:
orderBy = "DisplayNameLower";
break;
}
SortCrystalList(orderBy, ascendingOrder, crystalList);
}
/// <summary>
/// Sorts the Crystal list according to the public property in CrystalImageItem.
/// </summary>
/// <param name="sortType">Sort type to use.</param>
/// <param name="ascendingOrder">If true, sort by ascending order.</param>
/// <param name="reDrawGrid">If true, tells the active grid to repaint.</param>
public virtual void SortCrystalList(CrystalSortType sortType, bool ascendingOrder, bool reDrawGrid)
{
if (_gridModel == null)
return;
SortCrystalList(sortType, ascendingOrder, _gridModel.CrystalImageList);
// Have the model calculate its virtual size,
// and the size/position of each image item.
_gridModel.CalculateVirtualGrid();
// Tell the view to start drawing the image grid
if (reDrawGrid)
UpdateResizeVirtualGrid();
}
#endregion
#region Copy, Move, Delete, Rename, Select operations
/// <summary>
/// Selects the image nearest the index.
/// </summary>
/// <param name="imageIndex">Index of the image to select.</param>
public virtual void SelectImage(int imageIndex)
{
// Have the views select the images nearest the starting
// index of the item removed.
if (GridModel.Count > 0)
{
int newLimit = GridModel.Count - 1;
if (newLimit < imageIndex)
imageIndex = newLimit;
CrystalImageItem selectItem = GridModel[imageIndex];
if ((selectItem != null) && (GridView != null))
GridView.SelectImage(selectItem);
}
}
/// <summary>
/// Selects the image item that matches the image name.
/// Finds the item in the GridModel and selects it in the GridView.
/// </summary>
/// <param name="imageName">Name of the image item.</param>
/// <returns>True if item was found and selected, false otherwise.</returns>
public virtual bool SelectImage(string imageName)
{
if (GridModel == null)
return false;
CrystalImageItem searchItem = GridModel.FindImage(imageName, false, false);
if ((searchItem != null) && (GridView != null))
{
GridView.SelectImage(searchItem);
return true;
}
return false;
}
/// <summary>
/// Removes the images in the given list from the model.
/// </summary>
/// <param name="crystalImageList">List of images to be deleted from the model</param>
/// <returns>Number of images removed.</returns>
public virtual int RemoveImages(List<CrystalImageItem> crystalImageList)
{
int itemsRemoved = 0;
if (GridModel == null)
return itemsRemoved;
int selectIndex = GridModel.GetImageIndex(crystalImageList[0]);
for (int index = crystalImageList.Count - 1; index >= 0; index--)
{
CrystalImageItem theImageItem = crystalImageList[index];
if (theImageItem != null)
{
GridModel.CrystalImageList.Remove(theImageItem);
itemsRemoved++;
}
}
// Have the model calculate its virtual size,
// and the size/position of each image item.
_gridModel.CalculateVirtualGrid();
// Tell the view to start drawing the image grid
if (GridView != null)
{
UpdateResizeVirtualGrid();
// Select the image nearest the index.
SelectImage(selectIndex);
}
return itemsRemoved;
}
/// <summary>
/// Removes the images currently selected within the model.
/// </summary>
/// <param name="ownerWindow">Handle to the window that receives Shell dialogs.</param>
/// <returns>True if images are removed.</returns>
public virtual bool RemoveSelectedImages(IntPtr ownerWindow)
{
if (GridModel == null)
return false;
return (RemoveImages(GridModel.GetSelectedImageList()) > 0);
}
/// <summary>
/// Renames the image within the model.
/// </summary>
/// <param name="index">Valid index of the image inside the model.</param>
/// <param name="imageName">New name for the image.</param>
/// <param name="bRenameFileNow">If true, we rename the physical file.</param>
/// <param name="ownerWindow">Owner window to use for popup dialogs.</param>
/// <returns></returns>
public virtual bool RenameImage(int index, string imageName,
bool bRenameFileNow, IntPtr ownerWindow)
{
if (_gridModel != null)
{
if ((index >= 0) && (index < _gridModel.Count))
{
CrystalImageItem crystalImage = _gridModel[index];
if (crystalImage == null)
return false;
string oldFileName = crystalImage.ImageName;
crystalImage.ImageName = imageName;
crystalImage.DisplayName = Path.GetFileNameWithoutExtension(imageName);
// reset tooltip.
string toolTipText = crystalImage.ToolTipText;
crystalImage.ToolTipText = toolTipText.Replace(oldFileName, imageName);
UpdateSubscribers(crystalImage, index);
return true;
}
}
return false;
}
/// <summary>
/// Copies the image to the clipboard.
/// </summary>
/// <param name="crystalImage">Crystal Image object.</param>
public virtual void CopyToClipboard(CrystalImageItem crystalImage)
{
if (crystalImage == null)
return;
if (LoadImage(ref crystalImage))
Clipboard.SetDataObject(crystalImage.FullImage);
}
/// <summary>
/// Applies the image filter type to the view.
/// </summary>
/// <param name="crystalImageFilter">CrystalFilterType filter type.</param>
/// <param name="imageGridView">CrystalImageGridView control that will display the images according to the filter.</param>
public virtual void ApplyImageFilter(CrystalFilterType crystalImageFilter, CrystalImageGridView imageGridView)
{
if (imageGridView != null)
{
imageGridView.ImageItemFilter = crystalImageFilter;
CollectImages();
}
}
/// <summary>
/// Clears all cached thumbnail images within the image model.
/// </summary>
public virtual void ClearThumbnailImages()
{
if (GridModel == null)
return;
foreach (CrystalImageItem item in _gridModel)
{
item.ThumbnailImage = null;
item.OptThumbnailImage = null;
}
}
/// <summary>
/// Clears the optimized flag on all images within the image model.
/// </summary>
public virtual void ClearOptimizedThumbnailImages()
{
if (GridModel == null)
return;
foreach (CrystalImageItem item in _gridModel)
{
item.OptThumbnailImage = null;
}
}
/// <summary>
/// Retrives a thumbnail image from a crystal image item object, optimized for the object's imagerect.
/// </summary>
/// <param name="imageItem">CrystalImageItem containing data about the image.</param>
/// <returns>Thumbnail image if successful, false otherwise.</returns>
public virtual Image GetOptimizedThumbnailImage(ref CrystalImageItem imageItem)
{
if (imageItem == null)
return null;
if (imageItem.OptThumbnailImage != null)
{
return imageItem.OptThumbnailImage;
}
// Tell the controller to get the thumbnail image.
if (imageItem.ThumbnailImage == null)
{
LoadThumbnailImage(ref imageItem);
}
if (imageItem.ThumbnailImage != null)
{
Image thumbImage = imageItem.ThumbnailImage;
if ((thumbImage.Width > imageItem.ImageRect.Width) ||
(thumbImage.Height > imageItem.ImageRect.Height))
{
thumbImage = CrystalTools.GenerateThumbnail(thumbImage, imageItem.ImageRect);
}
imageItem.OptThumbnailImage = thumbImage;
return imageItem.OptThumbnailImage;
}
return null;
}
/// <summary>
/// Invalidates the image grid views associated with this controller.
/// </summary>
public virtual void InvalidateImageGridViews()
{
if (GridView != null)
{
if (GridView.InvokeRequired)
{
GridView.Invoke(new MethodInvoker(delegate
{
GridView.Invalidate();
}), null);
}
else
{
GridView.Invalidate();
}
}
}
/// <summary>
/// Clears the model data.
/// </summary>
public virtual void ClearModel()
{
if (_gridModel != null)
{
_gridModel.ClearModel();
}
}
#endregion
/// <summary>
/// Disposes the resources used by this object.
/// </summary>
public virtual void Dispose()
{
if (_gridModel != null)
{
_gridModel.Dispose();
}
CrystalLogger.LogEvent("CrystalCollector getting disposed.");
GC.SuppressFinalize(this);
}
}
}