using System;
using System.Collections.Generic;
using System.Windows;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using OptiTrack;
using System.Runtime.InteropServices;
using System.IO;
namespace SketchAssistantWPF
{
public class MVP_Model
{
///
/// The Presenter of the MVP-Model.
///
MVP_Presenter programPresenter;
///
/// History of Actions
///
ActionHistory historyOfActions;
///
/// The connector class used to get frames from the Optitrack system.
///
OptiTrackConnector connector;
/***********************/
/*** CLASS VARIABLES ***/
/***********************/
///
/// this is a variable used for detecting whether the tracker is in the warning zone (0 +- variable), no drawing zone (0 +- 2 * variable) or normal drawing zone
///
readonly double WARNING_ZONE_BOUNDARY = 0.10; //10cm
///
/// If the program is in drawing mode.
///
public bool inDrawingMode { get; private set; }
///
/// if the program is using OptiTrack
///
public bool optiTrackInUse { get; private set; }
///
/// Size of deletion area
///
int deletionRadius = 5;
///
/// 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 Position of the Cursor of opti track
///
Point currentOptiCursorPosition;
///
/// The Previous Cursor Position of opti track
///
Point previousOptiCursorPosition;
///
/// Queue for the cursorPositions of opti track
///
Queue optiCursorPositions = new Queue();
///
/// 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;
///
/// Width of the LeftImageBox.
///
public int leftImageBoxWidth;
///
/// Height of the LeftImageBox.
///
public int leftImageBoxHeight;
///
/// Width of the RightImageBox.
///
public int rightImageBoxWidth;
///
/// Height of the RightImageBox.
///
public int rightImageBoxHeight;
///
/// The size of the right canvas.
///
public ImageDimension rightImageSize { get; private set; }
///
/// Indicates whether or not the canvas on the right side is active.
///
public bool canvasActive { get; set; }
///
/// Indicates if there is a graphic loaded in the left canvas.
///
public bool graphicLoaded { get; set; }
///
/// Whether or not an optitrack system is avaiable.
///
public bool optitrackAvailable { get; private set; }
///
/// x coordinate in real world. one unit is one meter. If standing in front of video wall facing it, moving left results in incrementation of x.
///
public float optiTrackX;
///
/// y coordinate in real world. one unit is one meter. If standing in front of video wall, moving up results in incrementation of y.
///
public float optiTrackY;
///
/// z coordinate in real world. one unit is one meter. If standing in front of video wall, moving back results in incrementation of y.
///
public float optiTrackZ;
///
/// keeps track of whether last tick the trackable was inside drawing zone or not.
///
private bool optiTrackInsideDrawingZone = false;
///
/// object of class wristband used for controlling the vibrotactile wristband
///
private Wristband wristband;
///
/// Is set to true when the trackable has passed to the backside of the drawing surface,
/// invalidating all inputs on its way back.
///
bool OptiMovingBack = false;
///
/// The Layer in which the optitrack system was. 0 is drawing layer, -1 is in front, 1 is behind
///
int OptiLayer = 0;
///
/// The path traveled since the last tick
///
double PathTraveled = 0;
///
/// Whether or not the mouse is pressed.
///
private bool mouseDown;
///
/// A List of lines in the left canvas.
///
List leftLineList;
///
/// A list of lines in the right canvas along with a boolean indicating if they should be drawn.
///
List> rightLineList;
///
/// The line currently being drawin with optitrack.
///
List currentLine = new List();
public MVP_Model(MVP_Presenter presenter)
{
programPresenter = presenter;
historyOfActions = new ActionHistory();
rightLineList = new List>();
canvasActive = false;
UpdateUI();
rightImageSize = new ImageDimension(0, 0);
connector = new OptiTrackConnector();
wristband = new Wristband();
//Set up Optitrack
optitrackAvailable = false;
if (File.Exists(@"C:\Users\videowall-pc-user\Documents\BP-SketchAssistant\SketchAssistant\optitrack_setup.ttp"))
{
if (connector.Init(@"C:\Users\videowall-pc-user\Documents\BP-SketchAssistant\SketchAssistant\optitrack_setup.ttp"))
{
optitrackAvailable = true;
connector.StartTracking(GetOptiTrackPosition);
}
}
}
/**************************/
/*** INTERNAL FUNCTIONS ***/
/**************************/
///
/// Check if the Optitrack trackable is in the drawing plane.
///
/// The real world z coordinates of the trackable.
/// If the trackable is in front of the drawing plane
private bool CheckInsideDrawingZone(float optiTrackZ)
{
if (Math.Abs(optiTrackZ) > WARNING_ZONE_BOUNDARY * 2) return false;
return true;
}
///
/// Function that is called by the OptitrackController to pass frames to the model.
///
/// An Optitrack Frame
void GetOptiTrackPosition(OptiTrack.Frame frame)
{
if (frame.Trackables.Length >= 1)
{
optiTrackX = frame.Trackables[0].X;
optiTrackY = frame.Trackables[0].Y;
optiTrackZ = frame.Trackables[0].Z;
}
}
///
/// 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);
}
}
}
///
/// Check if enough distance has been travelled to warrant a vibration.
///
private void CheckPathTraveled()
{
var a = Math.Abs(previousOptiCursorPosition.X - currentOptiCursorPosition.X);
var b = Math.Abs(previousOptiCursorPosition.Y - currentOptiCursorPosition.Y);
PathTraveled += Math.Sqrt(Math.Pow(a,2) + Math.Pow(b,2));
//Set the Interval of vibrations here
if(PathTraveled > 2)
{
PathTraveled = 0;
//TODO: Activate vibration here
}
}
///
/// A function that populates the matrixes needed for deletion detection with line data.
///
private void RepopulateDeletionMatrixes()
{
if (canvasActive)
{
isFilledMatrix = new bool[rightImageSize.Width, rightImageSize.Height];
linesMatrix = new HashSet[rightImageSize.Width, rightImageSize.Height];
foreach (Tuple lineTuple in rightLineList)
{
if (lineTuple.Item1)
{
lineTuple.Item2.PopulateMatrixes(isFilledMatrix, linesMatrix);
}
}
}
}
///
/// Tells the Presenter to Update the UI
///
private void UpdateUI()
{
programPresenter.UpdateUIState(inDrawingMode, historyOfActions.CanUndo(), historyOfActions.CanRedo(), canvasActive, graphicLoaded, optitrackAvailable, optiTrackInUse);
}
///
/// 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 < rightImageSize.Width && pnt.Y < rightImageSize.Height)
{
if (isFilledMatrix[(int)pnt.X, (int)pnt.Y])
{
returnSet.UnionWith(linesMatrix[(int)pnt.X, (int)pnt.Y]);
}
}
}
return returnSet;
}
///
/// Converts given point to device-independent pixel.
///
/// real world coordinate
/// The given Point converted to device-independent pixel
private Point ConvertTo96thsOfInch(Point p)
{
//The position of the optitrack coordinate system
// and sizes of the optitrack tracking area relative to the canvas.
double OPTITRACK_X_OFFSET = -0.4854;
double OPTITRACK_Y_OFFSET = 0.9;
double OPTITRACK_CANVAS_HEIGHT = 1.2;
double OPTITRACK_X_SCALE = 0.21 * (((1.8316/*size of canvas*/ / 0.0254) * 96) / (1.8316));
double OPTITRACK_Y_SCALE = 0.205 * (((1.2 / 0.0254) * 96) / (1.2));
//The coordinates on the display
double xCoordinate = (p.X - OPTITRACK_X_OFFSET) * OPTITRACK_X_SCALE;
double yCoordinate = (OPTITRACK_CANVAS_HEIGHT - (p.Y - OPTITRACK_Y_OFFSET)) * OPTITRACK_Y_SCALE;
return new Point(xCoordinate, yCoordinate);
}
///
/// Updates the Optitrack coordiantes, aswell as the marker for optitrack.
///
/// The new cursor position
private void SetCurrentFingerPosition(Point p)
{
Point correctedPoint = ConvertTo96thsOfInch(p);
if (optiTrackZ < -2.2 * WARNING_ZONE_BOUNDARY && OptiLayer > -1)
{
OptiLayer = -1; OptiMovingBack = false;
programPresenter.SetOverlayColor("optipoint", Brushes.Yellow);
}
else if (optiTrackZ > 2 * WARNING_ZONE_BOUNDARY && OptiLayer < 1)
{
programPresenter.SetOverlayColor("optipoint", Brushes.Red);
OptiLayer = 1;
}
else if(optiTrackZ <= 2 * WARNING_ZONE_BOUNDARY && optiTrackZ >= -2.2 * WARNING_ZONE_BOUNDARY){
if(OptiLayer > 0)
OptiMovingBack = true;
programPresenter.SetOverlayColor("optipoint", Brushes.Green);
OptiLayer = 0;
}
currentOptiCursorPosition = correctedPoint;
programPresenter.MoveOptiPoint(currentOptiCursorPosition);
if (optiCursorPositions.Count > 0) { previousOptiCursorPosition = optiCursorPositions.Dequeue(); }
else { previousOptiCursorPosition = currentOptiCursorPosition; }
optiCursorPositions.Enqueue(currentOptiCursorPosition);
}
/********************************************/
/*** FUNCTIONS TO INTERACT WITH PRESENTER ***/
/********************************************/
///
/// A function to update the dimensions of the left and right canvas when the window is resized.
///
/// The size of the right canvas.
public void ResizeEvent(ImageDimension RightCanvas)
{
if (RightCanvas.Height >= 0 && RightCanvas.Width >= 0) { rightImageSize = RightCanvas; }
RepopulateDeletionMatrixes();
}
///
/// A function to reset the right image.
///
public void ResetRightImage()
{
if(currentLine.Count > 0)
FinishCurrentLine(true);
rightLineList.Clear();
programPresenter.PassLastActionTaken(historyOfActions.Reset());
programPresenter.ClearRightLines();
}
///
/// The function to set the left image.
///
/// The width of the left image.
/// The height of the left image.
/// The List of Lines to be displayed in the left image.
public void SetLeftLineList(int width, int height, List listOfLines)
{
rightImageSize = new ImageDimension(width, height);
leftLineList = listOfLines;
graphicLoaded = true;
programPresenter.UpdateLeftLines(leftLineList);
CanvasActivated();
}
///
/// A function to tell the model a new canvas was activated.
///
public void CanvasActivated()
{
canvasActive = true;
RepopulateDeletionMatrixes();
UpdateUI();
}
///
/// Will undo the last action taken, if the action history allows it.
///
public void Undo()
{
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;
}
programPresenter.UpdateRightLines(rightLineList);
}
RepopulateDeletionMatrixes();
programPresenter.PassLastActionTaken(historyOfActions.MoveAction(true));
UpdateUI();
}
///
/// Will redo the last action undone, if the action history allows it.
///
public void Redo()
{
if (historyOfActions.CanRedo())
{
programPresenter.PassLastActionTaken(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;
}
programPresenter.UpdateRightLines(rightLineList);
RepopulateDeletionMatrixes();
}
UpdateUI();
}
///
/// The function called by the Presenter to change the drawing state of the program.
///
/// The new drawingstate of the program
public void ChangeState(bool nowDrawing)
{
if(inDrawingMode && !nowDrawing && currentLine.Count > 0 && optiTrackInUse)
FinishCurrentLine(true);
inDrawingMode = nowDrawing;
UpdateUI();
}
///
/// The function called by the Presenter to set a variable which describes if OptiTrack is in use
///
/// The status of optitrack button
public void SetOptiTrack(bool usingOptiTrack)
{
optiTrackInUse = usingOptiTrack;
if (usingOptiTrack && optiTrackX == 0 && optiTrackY == 0 && optiTrackZ == 0)
{
programPresenter.PassWarning("Trackable not detected, please check if OptiTrack is activated and Trackable is recognized");
optiTrackInUse = false;
//Disable optipoint
programPresenter.SetOverlayStatus("optipoint", false, currentCursorPosition);
}
else
{
//Enable optipoint
programPresenter.SetOverlayStatus("optipoint", true, currentCursorPosition);
}
}
///
/// Updates the current cursor position of the model.
///
/// The new cursor position
public void SetCurrentCursorPosition(Point p)
{
currentCursorPosition = p;
mouseDown = programPresenter.IsMousePressed();
}
///
/// Start a new Line, when the Mouse is pressed down.
///
public void StartNewLine()
{
mouseDown = true;
if (inDrawingMode)
{
if(optiTrackInUse)
{
currentLine.Clear();
currentLine.Add(currentOptiCursorPosition);
}
else if (programPresenter.IsMousePressed())
{
currentLine.Clear();
currentLine.Add(currentCursorPosition);
}
}
}
///
/// Finish the current Line, when the pressed Mouse is released.
///
/// Whether the up event is valid or not
public void FinishCurrentLine(bool valid)
{
mouseDown = false;
if (valid)
{
if (inDrawingMode && currentLine.Count > 0)
{
InternalLine newLine = new InternalLine(currentLine, rightLineList.Count);
rightLineList.Add(new Tuple(true, newLine));
newLine.PopulateMatrixes(isFilledMatrix, linesMatrix);
programPresenter.PassLastActionTaken(historyOfActions.AddNewAction(new SketchAction(SketchAction.ActionType.Draw, newLine.GetID())));
//TODO: For the person implementing overlay: Add check if overlay needs to be added
programPresenter.UpdateRightLines(rightLineList);
currentLine.Clear();
programPresenter.UpdateCurrentLine(currentLine);
}
}
else
{
currentLine.Clear();
}
UpdateUI();
}
///
/// Finish the current Line, when the pressed Mouse is released.
/// Overload that is used to pass a list of points to be used when one is available.
///
/// The list of points
public void FinishCurrentLine(List p)
{
mouseDown = false;
if (inDrawingMode && currentLine.Count > 0)
{
InternalLine newLine = new InternalLine(p, rightLineList.Count);
rightLineList.Add(new Tuple(true, newLine));
newLine.PopulateMatrixes(isFilledMatrix, linesMatrix);
programPresenter.PassLastActionTaken(historyOfActions.AddNewAction(new SketchAction(SketchAction.ActionType.Draw, newLine.GetID())));
programPresenter.UpdateRightLines(rightLineList);
currentLine.Clear();
}
UpdateUI();
}
///
/// Method to be called every tick. Updates the current Line, or checks for Lines to delete, depending on the drawing mode.
///
public void Tick()
{
if (cursorPositions.Count > 0) { previousCursorPosition = cursorPositions.Dequeue(); }
else { previousCursorPosition = currentCursorPosition; }
if(optitrackAvailable)
SetCurrentFingerPosition(new Point(optiTrackX, optiTrackY));
if (optiTrackInUse && inDrawingMode && !OptiMovingBack) // optitrack is being used
{
//outside of drawing zone
if (!CheckInsideDrawingZone(optiTrackZ))
{
//Check if trackable was in drawing zone last tick & program is in drawing mode-> finish line
if (optiTrackInsideDrawingZone && inDrawingMode)
{
optiTrackInsideDrawingZone = false;
FinishCurrentLine(true);
}
}
else //Draw with optitrack, when in drawing zone
{
//Optitrack wasn't in the drawing zone last tick -> start a new line
if (!optiTrackInsideDrawingZone)
{
optiTrackInsideDrawingZone = true;
StartNewLine();
}
else currentLine.Add(currentOptiCursorPosition);
programPresenter.UpdateCurrentLine(currentLine);
if (optiTrackZ > WARNING_ZONE_BOUNDARY)
{
wristband.PushForward();
}
else if (optiTrackZ < -1 * WARNING_ZONE_BOUNDARY)
{
wristband.PushBackward();
}
}
}
else if( !optiTrackInUse && inDrawingMode)
{
//drawing without optitrack
cursorPositions.Enqueue(currentCursorPosition);
if (inDrawingMode && programPresenter.IsMousePressed())
{
currentLine.Add(currentCursorPosition);
//programPresenter.UpdateCurrentLine(currentLine);
}
}
//Deletion mode for optitrack and regular use
if (!inDrawingMode)
{
List uncheckedPoints = new List();
if (programPresenter.IsMousePressed() && !optiTrackInUse) //without optitrack
uncheckedPoints = GeometryCalculator.BresenhamLineAlgorithm(previousCursorPosition, currentCursorPosition);
if(optiTrackInUse && CheckInsideDrawingZone(optiTrackZ) && !OptiMovingBack) //with optitrack
uncheckedPoints = GeometryCalculator.BresenhamLineAlgorithm(previousOptiCursorPosition, currentOptiCursorPosition);
foreach (Point currPoint in uncheckedPoints)
{
HashSet linesToDelete = CheckDeletionMatrixesAroundPoint(currPoint, deletionRadius);
if (linesToDelete.Count > 0)
{
programPresenter.PassLastActionTaken(historyOfActions.AddNewAction(new SketchAction(SketchAction.ActionType.Delete, linesToDelete)));
foreach (int lineID in linesToDelete)
{
rightLineList[lineID] = new Tuple(false, rightLineList[lineID].Item2);
}
RepopulateDeletionMatrixes();
programPresenter.UpdateRightLines(rightLineList);
}
}
}
}
///
/// If there is unsaved progress.
///
/// True if there is progress that has not been saved.
public bool HasUnsavedProgress()
{
return !historyOfActions.IsEmpty();
}
}
}