using Inet.Viewer.Data;
using Inet.Viewer.WinForms;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Text;
namespace Inet.Viewer.Data
{
///
/// Represents the content of one page and manages the loading and updating of pre-renderings.
///
public class PageContent : IPageReceiver
{
private const int BorderOffset = 2;
private const int LoadingSpinDuration = 20000;
///
/// Sum of horizontal/vertical borders including any shadows, in pixels.
///
public const int BorderTotal = 2 * BorderOffset + 2;
private static readonly Brush BgBrush = new SolidBrush(Color.White);
private static readonly Brush BgLoadingBrush = new SolidBrush(Color.FromArgb(220, 220, 220));
private static readonly Pen BorderPen = new Pen(Color.FromArgb(190, 190, 190), 1);
private static readonly Pen ShadowPen1 = new Pen(Color.FromArgb(160, 160, 160), 2);
private static readonly Pen ShadowPen2 = new Pen(Color.FromArgb(80, 80, 80), 2);
private static readonly Brush HighlightBrush = new SolidBrush(Color.FromArgb(0x40, 0xff, 0xa0, 0x00));
private int pageNumber;
private PageInfo pageInfo;
private PageContentData data;
private WeakReference cachedData = new WeakReference(null);
private List selectedTexts = new List();
private SearchChunk[] highlightedSearchChunks;
private Graphics2DPainter painter;
private IPageReceiver masterReceiver;
private ReportDataCache dataCache;
private bool pageInfoReceived;
private float imageZoom;
///
/// Called after a page was rendered.
///
public event EventHandler PageRendered;
///
/// Creates a new instance.
///
/// the page number
/// the main page receiver
/// the data cache to read from
internal PageContent(int pageNumber, IPageReceiver masterReceiver, ReportDataCache dataCache)
{
this.pageNumber = pageNumber;
this.masterReceiver = masterReceiver;
this.dataCache = dataCache;
}
///
/// Clears any prerendering.
///
public void ClearRendering()
{
data = null;
painter = null;
}
///
/// Updates the pre-rendered image. If required, this method starts the actual loading/rendering in a background thread.
///
/// flag indicating that a reload will be forced
/// zoom factor, used to check if the zoom factor of the current prerendering is valid
public void Update(bool forceReload, float zoomFactor)
{
if (forceReload)
{
data = null;
painter = null;
}
else if (data == null)
{
// try to restore it from weak ref cache
data = (PageContentData)cachedData.Target;
UpdateSearchHighlighting();
}
Graphics2DPainter p = painter;
if (p == null && (data == null || Math.Abs(imageZoom - zoomFactor) > 0.001f) ||
p != null && pageInfoReceived && Math.Abs(painter.ZoomFactor - zoomFactor) > 0.001f)
{
painter = new Graphics2DPainter(false);
pageInfoReceived = false;
ThreadManager.RequestPageData(null, dataCache, pageNumber, forceReload, new PageLoader(painter, this), SetImage);
}
}
///
/// Sets the image (after the background thread is finished).
///
private void SetImage(Image img, IList links, IList texts, Graphics2DPainter painter)
{
if (painter == this.painter)
{
cachedData.Target = this.data = new PageContentData(img, links, texts);
this.imageZoom = painter.ZoomFactor;
this.painter = null;
UpdateSearchHighlighting();
if (PageRendered != null)
{
PageRendered.Invoke(this, new EventArgs());
}
}
else
{
painter.ClearElements();
}
}
///
/// Gets the page number.
///
public int PageNumber { get { return pageNumber; } }
///
///
///
public bool WriteReportInfo(ReportInfo info, PageLoader loader)
{
return masterReceiver.WriteReportInfo(info, loader);
}
///
///
///
public bool WritePageInfo(PageInfo info, PageLoader loader)
{
if (loader.Painter != painter || info.PageNr != PageNumber)
{
return false;
}
this.pageInfoReceived = true;
this.pageInfo = info;
return masterReceiver.WritePageInfo(info, loader);
}
///
///
///
public Font GetEmbeddedFont(int fontID, int fontRevision)
{
return masterReceiver.GetEmbeddedFont(fontID, fontRevision);
}
///
///
///
public void PageLoadFailure(Exception exception)
{
masterReceiver.PageLoadFailure(exception);
}
///
/// Draws the page. If the pre-rendered image is not available a loading spinner will be painted instead.
///
/// the graphics to paint into
/// the width of the image to paint
/// the height of the image to paint
/// if true a loading animation will be shown when the page is not available
/// returns true if a repaint is required because of a running animation
public bool Paint(Graphics g, int width, int height, bool showLoadingAnim)
{
PageContentData data = this.data;
if (data == null)
{
if (!showLoadingAnim)
{
return false;
}
g.FillRectangle(BgLoadingBrush, 0, 0, width, height);
g.DrawRectangle(BorderPen, 0, 0, width, height);
Matrix tx = g.Transform;
float scale = Math.Min(1f, (float)width / 2 / Images.spinner.Width);
g.TranslateTransform(width / 2, height / 2);
g.ScaleTransform(scale, scale);
PaintLoadingSpinner(g, new Point(0, 0));
g.Transform = tx;
return true;
}
Image image = data.image;
// draw image
if (width != image.Width || height != image.Height)
{
g.DrawImage(image, BorderOffset, BorderOffset, width, height);
}
else
{
g.DrawImage(image, BorderOffset, BorderOffset);
}
// draw page border and shadow
int x1 = width + BorderOffset;
int y1 = height + BorderOffset;
g.DrawLine(ShadowPen1, x1, 3, x1, y1);
g.DrawLine(ShadowPen1, 3, y1, x1, y1);
g.DrawLines(ShadowPen2, new Point[] { new Point(x1, 4), new Point(x1, y1), new Point(4, y1) });
g.DrawRectangle(BorderPen, 0, 0, width, height);
// draw highlighted texts
lock (selectedTexts)
{
Brush textBrush = new SolidBrush(Color.Black);
g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias;
foreach (TextBlockRange subTextBlock in selectedTexts)
{
RectangleF bbox = subTextBlock.BBox;
bbox.X -= 1;
bbox.Y -= 1;
bbox.Width += 4;
bbox.Height += 4;
g.FillRectangle(HighlightBrush, bbox);
}
}
return false;
}
///
/// Draws a loading spinner.
///
/// the graphics to draw to
/// the location of the spinner
internal static void PaintLoadingSpinner(Graphics g, Point point)
{
Matrix tx = g.Transform;
{
g.TranslateTransform(point.X, point.Y);
g.RotateTransform((float)(System.DateTime.Now.Ticks % (360 * LoadingSpinDuration)) / LoadingSpinDuration);
g.TranslateTransform(-Images.spinner.Width / 2, -Images.spinner.Height / 2);
g.DrawImage(Images.spinner, 0, 0, Images.spinner.Width, Images.spinner.Height);
}
g.Transform = tx;
}
///
/// Clears any selected text and hightlighted search chunks.
///
public void ClearSelection()
{
lock (selectedTexts)
{
selectedTexts.Clear();
highlightedSearchChunks = null;
}
}
///
/// Update the text selection in respect to the search chunks.
///
private void UpdateSearchHighlighting()
{
lock (selectedTexts)
{
selectedTexts.Clear();
var data = this.data;
if (data == null || data.texts == null || highlightedSearchChunks == null)
{
return;
}
foreach (SearchChunk chunk in highlightedSearchChunks)
{
foreach (TextBlock text in data.texts)
{
if (chunk.Page == pageNumber && text.HasDocumentLocation(chunk.X, chunk.Y))
{
selectedTexts.Add(new TextBlockRange(text, chunk.StartIndex, chunk.EndIndex));
}
}
}
}
}
///
/// Selects the text in the specified rectangle.
///
///
public void SelectArea(Rectangle rectangle)
{
lock (selectedTexts)
{
if (data == null)
{
return;
}
IList texts = data.texts;
if (texts == null)
{
return;
}
rectangle.X -= 16;
rectangle.Y -= 16;
foreach (TextBlock textBlock in texts)
{
TextBlockRange range = textBlock.ComputeRangeForArea(rectangle);
if (range != null)
{
selectedTexts.Add(range);
}
}
}
}
///
/// Appends any selected text to the specified string builder.
///
/// the string builder to append to
internal void AppendSelectedText(StringBuilder sb)
{
lock (selectedTexts)
{
if (selectedTexts.Count == 0)
{
return;
}
selectedTexts.Sort(CompareTextBlockY);
float prevY = 0;
bool first = true;
foreach (TextBlockRange range in selectedTexts)
{
if (first)
{
first = false;
}
else
{
sb.Append(range.BBox.Y > prevY ? Environment.NewLine : " ");
}
sb.Append(range.SubString);
prevY = range.BBox.Y + range.BBox.Height/2;
}
sb.Append('\n');
}
}
///
/// Comparer function to sort text blocks by their x/y positions.
///
/// first block
/// second block
/// order value -1,0 or 1
private static int CompareTextBlockY(TextBlockRange a, TextBlockRange b)
{
RectangleF abox = a.BBox;
RectangleF bbox = b.BBox;
if (abox.Y + abox.Height/2 < bbox.Y)
{
return -1;
}
else if (abox.Y > bbox.Y + bbox.Height/2 || abox.X < bbox.X)
{
return 1;
}
else if (abox.X == bbox.X || abox.Y + abox.Height / 2 == bbox.Y || abox.Y == bbox.Y + bbox.Height / 2)
{
return 0;
}
else
{
return -1;
}
}
///
/// Sets the array of hightlighted search chunks.
///
public SearchChunk[] HighlightedSearchChunks
{
set
{
highlightedSearchChunks = value;
if (data != null)
{
UpdateSearchHighlighting();
}
}
}
///
/// Gets the links on the page or null if not loaded.
///
internal IList Links
{
get {
var data = this.data;
return data == null ? null : data.links;
}
}
///
/// Encapsulates the data objects to make them cacheable with one weak reference.
///
private class PageContentData
{
public Image image;
public IList links;
public IList texts;
public PageContentData(Image image, IList links, IList texts)
{
this.image = image;
this.links = links;
this.texts = texts;
}
}
}
}