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; /// /// intensity of the tectile feedback occuring once every passed centimeter (ranging from 0.0 to 1.0) /// static readonly double TACTILE_SURFACE_FEEDBACK_INTENSITY = 1.0; /// /// duration of the tectile feedback occuring once every passed centimeter (in milliseconds) /// static readonly int TACTILE_SURFACE_FEEDBACK_DURATION = 50; /***********************/ /*** 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) { //TODO remove Console.WriteLine("trying to initialize Armband..."); int tmp= LocalArmbandInterface.SetupArmband(); Console.WriteLine("Armband initialization terminated, exit code: " + tmp); 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; LocalArmbandInterface.Actuate(0, TACTILE_SURFACE_FEEDBACK_INTENSITY, TACTILE_SURFACE_FEEDBACK_DURATION); } } /// /// 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(); } CheckPathTraveled(); } } 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 (programPresenter.IsMousePressed()) //TODO only use if optitrack is in use (currently only available on different branch) { if(CentimeterGridPassed(previousCursorPosition, currentCursorPosition)) //TODO replace with optitrack coordinates (and introduce a buffer for previous real-world coordinates) { LocalArmbandInterface.Actuate(0, TACTILE_SURFACE_FEEDBACK_INTENSITY, TACTILE_SURFACE_FEEDBACK_DURATION); } } */ } /// /// checks if the centimeter grid has been passed since the last tick and a vibrotactile feedback has to be sent to the user /// a high enough tick rate must be ensured to provide a useful feedback and avoid skipping necessary feedback /// /// the curser position in real world coordinates (obtained from Optitrack) at the last tick /// the curser position in real world coordinates (obtained from Optitrack) at the current tick /// true iff the grid has been passed private bool CentimeterGridPassed(Point previousCursorPositionRealWorldCoordinates, Point currentCursorPositionRealWorldCoordinates) { //truncate coordiates to int after converting to centimeters (from meters), if ((int)(previousCursorPositionRealWorldCoordinates.X * 100) != (int)(currentCursorPositionRealWorldCoordinates.X * 100)) return true; if ((int)(previousCursorPositionRealWorldCoordinates.Y * 100) != (int)(currentCursorPositionRealWorldCoordinates.Y * 100)) return true; return false; } /* /// /// A helper Function that updates the markerRadius & deletionRadius, considering the size of the canvas. /// /// The size of the canvas public void UpdateSizes(ImageDimension CanvasSize) { if (rightImageWithoutOverlay != null) { int widthImage = rightImageSize.Width; int heightImage = rightImageSize.Height; int widthBox = CanvasSize.Width; int heightBox = CanvasSize.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); deletionRadius = (int)(5 * zoomFactor); } } */ /// /// If there is unsaved progress. /// /// True if there is progress that has not been saved. public bool HasUnsavedProgress() { return !historyOfActions.IsEmpty(); } } }