using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Text.RegularExpressions;
// This is the code for your desktop app.
// Press Ctrl+F5 (or go to Debug > Start Without Debugging) to run your app.
namespace SketchAssistant
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
fileImporter = new FileImporter(this);
}
/**********************************/
/*** CLASS VARIABLES START HERE ***/
/**********************************/
//important: add new variables only at the end of the list to keep the order of definition consistent with the order in which they are returned by GetAllVariables()
///
/// Different Program States
///
public enum ProgramState
{
Idle,
Draw,
Delete
}
///
/// Current Program State
///
private ProgramState currentState;
///
/// instance of FileImporter to handle drawing imports
///
private FileImporter fileImporter;
///
/// Dialog to select a file.
///
OpenFileDialog openFileDialog = new OpenFileDialog();
///
/// Image loaded on the left
///
private Image leftImage = null;
///
/// the graphic shown in the left window, represented as a list of polylines
///
private List leftLineList;
///
/// Image on the right
///
Image rightImage = null;
///
/// Current Line being Drawn
///
List currentLine;
///
/// All Lines in the current session
///
List> rightLineList = new List>();
///
/// Whether the Mouse is currently pressed in the rightPictureBox
///
bool mousePressed = false;
///
/// The Position of the Cursor in the right picture box
///
Point currentCursorPosition;
///
/// The Previous Cursor Position in the right picture box
///
Point previousCursorPosition;
///
/// Queue for the cursorPositions
///
Queue cursorPositions = new Queue();
///
/// The graphic representation of the right image
///
Graphics rightGraph = null;
///
/// Lookup Matrix for checking postions of lines in the image
///
bool[,] isFilledMatrix;
///
/// Lookup Matrix for getting line ids at a certain postions of the image
///
HashSet[,] linesMatrix;
///
/// Size of deletion area
///
int deletionRadius = 2;
///
/// History of Actions
///
ActionHistory historyOfActions;
///
/// List of items which will be overlayed over the right canvas.
///
List> overlayItems;
///
/// The assistant responsible for the redraw mode
///
RedrawAssistant redrawAss;
///
/// Size of areas marking endpoints of lines in the redraw mode.
///
int markerRadius = 10;
/******************************************/
/*** FORM SPECIFIC FUNCTIONS START HERE ***/
/******************************************/
private void Form1_Load(object sender, EventArgs e)
{
currentState = ProgramState.Idle;
this.DoubleBuffered = true;
historyOfActions = new ActionHistory(null);
redrawAss = new RedrawAssistant();
UpdateButtonStatus();
}
///
/// Resize Function connected to the form resize event, will refresh the form when it is resized
///
private void Form1_Resize(object sender, System.EventArgs e)
{
this.Refresh();
UpdateSizes();
}
//Load button, will open an OpenFileDialog
private void loadToolStripMenuItem_Click(object sender, EventArgs e)
{
openFileDialog.Filter = "Image|*.jpg;*.png;*.jpeg";
if(openFileDialog.ShowDialog() == DialogResult.OK)
{
toolStripLoadStatus.Text = openFileDialog.SafeFileName;
leftImage = Image.FromFile(openFileDialog.FileName);
pictureBoxLeft.Image = leftImage;
//Refresh the left image box when the content is changed
this.Refresh();
}
UpdateButtonStatus();
}
///
/// Import button, will open an OpenFileDialog
///
private void examplePictureToolStripMenuItem_Click(object sender, EventArgs e)
{
if (CheckSavedStatus())
{
openFileDialog.Filter = "Interactive Sketch-Assistant Drawing|*.isad";
if (openFileDialog.ShowDialog() == DialogResult.OK)
{
toolStripLoadStatus.Text = openFileDialog.SafeFileName;
try
{
(int, int, List) values = fileImporter.ParseISADInputFile(openFileDialog.FileName);
DrawEmptyCanvasLeft(values.Item1, values.Item2);
BindAndDrawLeftImage(values.Item3);
//Match The right canvas to the left
historyOfActions = new ActionHistory(lastActionTakenLabel);
DrawEmptyCanvasRight();
isFilledMatrix = new bool[rightImage.Width, rightImage.Height];
linesMatrix = new HashSet[rightImage.Width, rightImage.Height];
rightLineList = new List>();
//Start the redraw mode
redrawAss = new RedrawAssistant(leftLineList);
UpdateSizes();
overlayItems = redrawAss.Tick(currentCursorPosition, rightLineList, -1, false);
RedrawRightImage();
this.Refresh();
}
catch (FileImporterException ex)
{
ShowInfoMessage(ex.ToString());
}
}
}
UpdateButtonStatus();
}
///
/// Changes the state of the program to drawing
///
private void drawButton_Click(object sender, EventArgs e)
{
if(rightImage != null)
{
if (currentState.Equals(ProgramState.Draw))
{
ChangeState(ProgramState.Idle);
}
else
{
ChangeState(ProgramState.Draw);
}
}
UpdateButtonStatus();
}
///
/// Changes the state of the program to deletion
///
private void deleteButton_Click(object sender, EventArgs e)
{
if (rightImage != null)
{
if (currentState.Equals(ProgramState.Delete))
{
ChangeState(ProgramState.Idle);
}
else
{
ChangeState(ProgramState.Delete);
}
}
UpdateButtonStatus();
}
///
/// Undo an Action.
///
private void undoButton_Click(object sender, EventArgs e)
{
if (historyOfActions.CanUndo())
{
HashSet affectedLines = historyOfActions.GetCurrentAction().GetLineIDs();
SketchAction.ActionType undoAction = historyOfActions.GetCurrentAction().GetActionType();
switch (undoAction)
{
case SketchAction.ActionType.Delete:
//Deleted Lines need to be shown
ChangeLines(affectedLines, true);
break;
case SketchAction.ActionType.Draw:
//Drawn lines need to be hidden
ChangeLines(affectedLines, false);
break;
default:
break;
}
overlayItems = redrawAss.Tick(currentCursorPosition, rightLineList, -1, false);
RedrawRightImage();
}
historyOfActions.MoveAction(true);
UpdateButtonStatus();
}
///
/// Redo an Action.
///
private void redoButton_Click(object sender, EventArgs e)
{
if (historyOfActions.CanRedo())
{
historyOfActions.MoveAction(false);
HashSet affectedLines = historyOfActions.GetCurrentAction().GetLineIDs();
SketchAction.ActionType redoAction = historyOfActions.GetCurrentAction().GetActionType();
switch (redoAction)
{
case SketchAction.ActionType.Delete:
//Deleted Lines need to be redeleted
ChangeLines(affectedLines, false);
break;
case SketchAction.ActionType.Draw:
//Drawn lines need to be redrawn
ChangeLines(affectedLines, true);
break;
default:
break;
}
overlayItems = redrawAss.Tick(currentCursorPosition, rightLineList, -1, false);
RedrawRightImage();
}
UpdateButtonStatus();
}
///
/// Detect Keyboard Shortcuts.
///
private void Form1_KeyDown(object sender, KeyEventArgs e)
{
if (e.Modifiers == Keys.Control && e.KeyCode == Keys.Z)
{
undoButton_Click(sender, e);
}
if (e.Modifiers == Keys.Control && e.KeyCode == Keys.Y)
{
redoButton_Click(sender, e);
}
}
///
/// Get current Mouse positon within the right picture box.
///
private void pictureBoxRight_MouseMove(object sender, MouseEventArgs e)
{
currentCursorPosition = ConvertCoordinates(new Point(e.X, e.Y));
}
///
/// Hold left mouse button to start drawing.
///
private void pictureBoxRight_MouseDown(object sender, MouseEventArgs e)
{
mousePressed = true;
if (currentState.Equals(ProgramState.Draw))
{
currentLine = new List();
}
}
///
/// Lift left mouse button to stop drawing and add a new Line.
///
private void pictureBoxRight_MouseUp(object sender, MouseEventArgs e)
{
mousePressed = false;
if (currentState.Equals(ProgramState.Draw) && currentLine.Count > 0)
{
Line newLine = new Line(currentLine, rightLineList.Count);
rightLineList.Add(new Tuple(true, newLine));
newLine.PopulateMatrixes(isFilledMatrix, linesMatrix);
historyOfActions.AddNewAction(new SketchAction(SketchAction.ActionType.Draw, newLine.GetID()));
//Execute a RedrawAssistant tick with the currently finished Line
overlayItems = redrawAss.Tick(currentCursorPosition, rightLineList, newLine.GetID(), true);
RedrawRightImage();
}
UpdateButtonStatus();
}
///
/// Button to create a new Canvas. Will create an empty image
/// which is the size of the left image, if there is one.
/// If there is no image loaded the canvas will be the size of the right picture box
///
private void canvasButton_Click(object sender, EventArgs e)
{
if (CheckSavedStatus())
{
historyOfActions = new ActionHistory(lastActionTakenLabel);
DrawEmptyCanvasRight();
//The following lines cannot be in DrawEmptyCanvas()
isFilledMatrix = new bool[rightImage.Width, rightImage.Height];
linesMatrix = new HashSet[rightImage.Width, rightImage.Height];
rightLineList = new List>();
//Reinitialise the Redraw Assistant.
if(leftLineList != null)
{
redrawAss = new RedrawAssistant(leftLineList);
UpdateSizes();
overlayItems = redrawAss.Tick(currentCursorPosition, rightLineList, -1, false);
RedrawRightImage();
}
}
UpdateButtonStatus();
UpdateSizes();
}
///
/// Add a Point on every tick to the Drawpath.
/// Or detect lines for deletion on every tick
///
private void mouseTimer_Tick(object sender, EventArgs e)
{
if(cursorPositions.Count > 0) { previousCursorPosition = cursorPositions.Dequeue(); }
else { previousCursorPosition = currentCursorPosition; }
cursorPositions.Enqueue(currentCursorPosition);
//Drawing
if (currentState.Equals(ProgramState.Draw) && mousePressed)
{
rightGraph = Graphics.FromImage(rightImage);
currentLine.Add(currentCursorPosition);
Line drawline = new Line(currentLine);
drawline.DrawLine(rightGraph);
pictureBoxRight.Image = rightImage;
//Redraw overlay gets ticked
overlayItems = redrawAss.Tick(currentCursorPosition, rightLineList, rightLineList.Count, false);
RedrawRightImage();
}
//Deleting
if (currentState.Equals(ProgramState.Delete) && mousePressed)
{
List uncheckedPoints = GeometryCalculator.BresenhamLineAlgorithm(previousCursorPosition, currentCursorPosition);
foreach (Point currPoint in uncheckedPoints)
{
HashSet linesToDelete = CheckDeletionMatrixesAroundPoint(currPoint, deletionRadius);
if (linesToDelete.Count > 0)
{
historyOfActions.AddNewAction(new SketchAction(SketchAction.ActionType.Delete, linesToDelete));
foreach (int lineID in linesToDelete)
{
rightLineList[lineID] = new Tuple(false, rightLineList[lineID].Item2);
}
RepopulateDeletionMatrixes();
//Redraw overlay gets ticked
overlayItems = redrawAss.Tick(currentCursorPosition, rightLineList, -1, false);
RedrawRightImage();
}
}
}
}
/***********************************/
/*** HELPER FUNCTIONS START HERE ***/
/***********************************/
///
/// A function that returns a white canvas for a given width and height.
///
/// The width of the canvas in pixels
/// The height of the canvas in pixels
/// The new canvas
private Image GetEmptyCanvas(int width, int height)
{
Image image;
try
{
image = new Bitmap(width, height);
}
catch(ArgumentException e)
{
ShowInfoMessage("The requested canvas size caused an error: \n" + e.ToString() + "\n The Canvas will be set to match your window.");
image = new Bitmap(pictureBoxLeft.Width, pictureBoxLeft.Height);
}
Graphics graph = Graphics.FromImage(image);
graph.FillRectangle(Brushes.White, 0, 0, width + 10, height + 10);
return image;
}
///
/// Creates an empty Canvas
///
private void DrawEmptyCanvasRight()
{
if (leftImage == null)
{
SetAndRefreshRightImage(GetEmptyCanvas(pictureBoxRight.Width, pictureBoxRight.Height));
}
else
{
SetAndRefreshRightImage(GetEmptyCanvas(leftImage.Width, leftImage.Height));
}
}
///
/// Creates an empty Canvas on the left
///
/// width of the new canvas in pixels
/// height of the new canvas in pixels
private void DrawEmptyCanvasLeft(int width, int height)
{
if (width == 0)
{
SetAndRefreshLeftImage(GetEmptyCanvas(pictureBoxLeft.Width, pictureBoxLeft.Height));
}
else
{
SetAndRefreshLeftImage(GetEmptyCanvas(width, height));
}
}
///
/// Redraws all lines in lineList, for which their associated boolean value equals true.
///
private void RedrawRightImage()
{
var workingCanvas = GetEmptyCanvas(rightImage.Width, rightImage.Height);
var workingGraph = Graphics.FromImage(workingCanvas);
//Lines
foreach (Tuple lineBoolTuple in rightLineList)
{
if (lineBoolTuple.Item1)
{
lineBoolTuple.Item2.DrawLine(workingGraph);
}
}
//The Line being currently drawn
if(currentLine != null && currentLine.Count > 0 && currentState.Equals(ProgramState.Draw) && mousePressed)
{
var currLine = new Line(currentLine);
currLine.DrawLine(workingGraph);
}
//Overlay Items
foreach (HashSet item in overlayItems)
{
foreach(Point p in item)
{
workingGraph.FillRectangle(Brushes.Green, p.X, p.Y, 1, 1);
}
}
SetAndRefreshRightImage(workingCanvas);
}
///
/// A function to set rightImage and to refresh the respective PictureBox with it.
///
/// The new Image
private void SetAndRefreshRightImage(Image image)
{
rightImage = image;
pictureBoxRight.Image = rightImage;
pictureBoxRight.Refresh();
}
///
/// A function to set leftImage and to refresh the respective PictureBox with it.
///
/// The new Image
private void SetAndRefreshLeftImage(Image image)
{
leftImage = image;
pictureBoxLeft.Image = leftImage;
pictureBoxLeft.Refresh();
}
///
/// Change the status of whether or not the lines are shown.
///
/// The HashSet containing the affected Line IDs.
/// True if the lines should be shown, false if they should be hidden.
private void ChangeLines(HashSet lines, bool shown)
{
foreach (int lineId in lines)
{
if (lineId <= rightLineList.Count - 1 && lineId >= 0)
{
rightLineList[lineId] = new Tuple(shown, rightLineList[lineId].Item2);
}
}
RedrawRightImage();
}
///
/// Updates the active status of buttons. Currently draw, delete, undo and redo button.
///
private void UpdateButtonStatus()
{
undoButton.Enabled = historyOfActions.CanUndo();
redoButton.Enabled = historyOfActions.CanRedo();
drawButton.Enabled = (rightImage != null);
deleteButton.Enabled = (rightImage != null);
}
///
/// A helper function which handles tasks associated witch changing states,
/// such as checking and unchecking buttons and changing the state.
///
/// The new state of the program
private void ChangeState(ProgramState newState)
{
switch (currentState)
{
case ProgramState.Draw:
drawButton.CheckState = CheckState.Unchecked;
mouseTimer.Enabled = false;
break;
case ProgramState.Delete:
deleteButton.CheckState = CheckState.Unchecked;
mouseTimer.Enabled = false;
break;
default:
break;
}
switch (newState)
{
case ProgramState.Draw:
drawButton.CheckState = CheckState.Checked;
mouseTimer.Enabled = true;
break;
case ProgramState.Delete:
deleteButton.CheckState = CheckState.Checked;
mouseTimer.Enabled = true;
break;
default:
break;
}
currentState = newState;
pictureBoxRight.Refresh();
}
///
/// A function that calculates the coordinates of a point on a zoomed in image.
///
/// The position of the mouse cursor
/// The real coordinates of the mouse cursor on the image
private Point ConvertCoordinates(Point cursorPosition)
{
Point realCoordinates = new Point(5,3);
if(pictureBoxRight.Image == null)
{
return cursorPosition;
}
int widthImage = pictureBoxRight.Image.Width;
int heightImage = pictureBoxRight.Image.Height;
int widthBox = pictureBoxRight.Width;
int heightBox = pictureBoxRight.Height;
float imageRatio = (float)widthImage / (float)heightImage;
float containerRatio = (float)widthBox / (float)heightBox;
if (imageRatio >= containerRatio)
{
//Image is wider than it is high
float zoomFactor = (float)widthImage / (float)widthBox;
float scaledHeight = heightImage / zoomFactor;
float filler = (heightBox - scaledHeight) / 2;
realCoordinates.X = (int)(cursorPosition.X * zoomFactor);
realCoordinates.Y = (int)((cursorPosition.Y - filler) * zoomFactor);
}
else
{
//Image is higher than it is wide
float zoomFactor = (float)heightImage / (float)heightBox;
float scaledWidth = widthImage / zoomFactor;
float filler = (widthBox - scaledWidth) / 2;
realCoordinates.X = (int)((cursorPosition.X - filler) * zoomFactor);
realCoordinates.Y = (int)(cursorPosition.Y * zoomFactor);
}
return realCoordinates;
}
///
/// A function that populates the matrixes needed for deletion detection with line data.
///
private void RepopulateDeletionMatrixes()
{
if(rightImage != null)
{
isFilledMatrix = new bool[rightImage.Width,rightImage.Height];
linesMatrix = new HashSet[rightImage.Width, rightImage.Height];
foreach(Tuple lineTuple in rightLineList)
{
if (lineTuple.Item1)
{
lineTuple.Item2.PopulateMatrixes(isFilledMatrix, linesMatrix);
}
}
}
}
///
/// A function that checks the deletion matrixes at a certain point
/// and returns all Line ids at that point and in a square around it in a certain range.
///
/// The point around which to check.
/// The range around the point. If range is 0, only the point is checked.
/// A List of all lines.
private HashSet CheckDeletionMatrixesAroundPoint(Point p, int range)
{
HashSet returnSet = new HashSet();
foreach(Point pnt in GeometryCalculator.FilledCircleAlgorithm(p, (int)range))
{
if(pnt.X >= 0 && pnt.Y >= 0 && pnt.X < rightImage.Width && pnt.Y < rightImage.Height)
{
if (isFilledMatrix[pnt.X, pnt.Y])
{
returnSet.UnionWith(linesMatrix[pnt.X, pnt.Y]);
}
}
}
return returnSet;
}
///
/// binds the given picture to templatePicture and draws it
///
/// the new template picture, represented as a list of polylines
///
private void BindAndDrawLeftImage(List newTemplatePicture)
{
leftLineList = newTemplatePicture;
foreach(Line l in leftLineList)
{
l.DrawLine(Graphics.FromImage(leftImage));
}
}
///
/// shows the given info message in a popup and asks the user to aknowledge it
///
/// the message to show
private void ShowInfoMessage(String message)
{
MessageBox.Show(message);
}
///
/// Will calculate the start and endpoints of the given line on the right canvas.
///
/// The line.
/// The size of the circle with which the endpoints of the line are marked.
private Tuple, HashSet> CalculateStartAndEnd(Line line, int size)
{
var circle0 = GeometryCalculator.FilledCircleAlgorithm(line.GetStartPoint(), size);
var circle1 = GeometryCalculator.FilledCircleAlgorithm(line.GetEndPoint(), size);
var currentLineEndings = new Tuple, HashSet>(circle0, circle1);
return currentLineEndings;
}
///
/// A helper Function that updates the markerRadius & deletionRadius, considering the size of the canvas.
///
private void UpdateSizes()
{
if (rightImage != null)
{
int widthImage = pictureBoxRight.Image.Width;
int heightImage = pictureBoxRight.Image.Height;
int widthBox = pictureBoxRight.Width;
int heightBox = pictureBoxRight.Height;
float imageRatio = (float)widthImage / (float)heightImage;
float containerRatio = (float)widthBox / (float)heightBox;
float zoomFactor = 0;
if (imageRatio >= containerRatio)
{
//Image is wider than it is high
zoomFactor = (float)widthImage / (float)widthBox;
}
else
{
//Image is higher than it is wide
zoomFactor = (float)heightImage / (float)heightBox;
}
markerRadius = (int)(10 * zoomFactor);
redrawAss.SetMarkerRadius(markerRadius);
deletionRadius = (int)(5 * zoomFactor);
}
}
///
/// Checks if there is unsaved progess, and warns the user. Returns True if it safe to continue.
///
/// true if there is none, or the user wishes to continue without saving.
/// false if there is progress, and the user doesn't wish to continue.
private bool CheckSavedStatus()
{
if (!historyOfActions.IsEmpty())
{
return (MessageBox.Show("You have unsaved changes, do you wish to continue?",
"Attention", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) == DialogResult.Yes);
}
return true;
}
/********************************************/
/*** TESTING RELATED FUNCTIONS START HERE ***/
/********************************************/
///
/// returns all instance variables in the order of their definition for testing
///
/// A list of tuples containing names of variables and the variable themselves.
/// Cast according to the Type definitions in the class variable section.
public List>GetAllVariables()
{
var objArr = new (String, object)[] { ("currentState", currentState), ("fileImporter", fileImporter), ("openFileDialog", openFileDialog),
("leftImage", leftImage), ("leftLineList", leftLineList), ("rightImage", rightImage), ("currentLine", currentLine),
("rightLineList", rightLineList), ("mousePressed", mousePressed), ("currentCursorPosition", currentCursorPosition),
("previousCursorPosition", previousCursorPosition), ("cursorPositions", cursorPositions), ("rightGraph", rightGraph),
("isFilledMatrix", isFilledMatrix), ("linesMatrix", linesMatrix), ("deletionRadius", deletionRadius),
("historyOfActions", historyOfActions), ("overlayItems", overlayItems), ("redrawAss", redrawAss), ("markerRadius", markerRadius) };
var varArr = new List>();
foreach((String, object) obj in objArr)
{
varArr.Add(new Tuple(obj.Item1, obj.Item2));
}
return varArr;
}
///
/// public method wrapper for testing purposes, invoking DrawEmptyCanvas(...) and BindAndDrawLeftImage(...)
///
/// width of the parsed image
/// height of the parsed image
/// the parsed image
public void CreateCanvasAndSetPictureForTesting(int width, int height, List newImage)
{
DrawEmptyCanvasLeft(width, height);
BindAndDrawLeftImage(newImage);
}
}
}