* Modify a bit the parasite nucleus detection sensibility.
[master-thesis.git] / Parasitemia / ParasitemiaUI / GUI.fs
index 98b13ea..3746461 100644 (file)
@@ -24,17 +24,17 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
     let mainWindow = Views.MainWindow()
     let ctrl (name: string): 'a = mainWindow.Root.FindName(name) :?> 'a
 
-    let colorRBCHealthy = Brushes.YellowGreen
-    let colorRBCInfected = Brushes.Red
-
-    let state = State.State()
+    let state = State.State(defaultConfig)
     let mutable currentScale = 1.
     let mutable displayHealthy = false
+    let warningBelowNumberOfRBC = 1000
 
     let menuExit: MenuItem = ctrl "menuExit"
     let menuSaveFile: MenuItem = ctrl "menuSave"
+    let menuSaveAsFile: MenuItem = ctrl "menuSaveAs"
     let menuLoadFile: MenuItem = ctrl "menuOpen"
     let menuNewFile: MenuItem = ctrl "menuNew"
+    let menuExportResults: MenuItem = ctrl "menuExportResults"
     let menuAddSourceImage: MenuItem = ctrl "menuAddSourceImage"
     let menuAnalysis: MenuItem = ctrl "menuAnalysis"
     let menuStartAnalysis: MenuItem = ctrl "menuStartAnalysis"
@@ -42,20 +42,30 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
     let menuHightlightRBC: MenuItem = ctrl "menuHightlightRBC"
     let menuAbout: MenuItem = ctrl "menuAbout"
 
+    let txtDocumentStatus: TextBlock = ctrl "txtDocumentStatus"
+    let txtMessageStatus: TextBlock = ctrl "txtMessageStatus"
+
     let txtPatient: TextBox = ctrl "txtPatient"
-    let txtGlobalParasitemia: TextBox = ctrl "txtGlobalParasitemia"
+    let txtGlobalParasitemia: TextBlock = ctrl "txtGlobalParasitemia"
 
     let stackPreviews: StackPanel = ctrl "stackPreviews"
 
     let scrollViewCurrentImage: ScrollViewer = ctrl "scrollViewCurrentImage"
     let borderCurrentImage: Border = ctrl "borderCurrentImage"
     let canvasCurrentImage: Canvas = ctrl "canvasCurrentImage"
-    let txtImageInformation: TextBlock = ctrl "txtImageInformation"
+
+    let gridImageInformation: Grid = ctrl "gridImageInformation"
+    let txtImageInformation1: TextBlock = ctrl "txtImageInformation1"
+    let txtImageInformation2: TextBlock = ctrl "txtImageInformation2"
+    let txtImageName: TextBox = ctrl "txtImageName"
 
     let scrollRBC: ScrollViewer = ctrl "scrollRBC"
     let stackRBC: StackPanel = ctrl "stackRBC"
 
+    let imgLogos: Border = ctrl "imgLogos"
+
     // Initializations.
+    let canvasCurrentImageColor = canvasCurrentImage.Background
     menuHightlightRBC.IsChecked <- displayHealthy
 
     // Utils.
@@ -70,23 +80,34 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
                                                 System.Drawing.Size((if x + w >= img.Width then img.Width - x else w),
                                                                     (if y + h >= img.Height then img.Height - y else h))))
 
-    let setRBCFrameStyle (rbc: RBC) (frame: Views.RBCFrame) =
+    let setRBCFrameStyle (srcImg: SourceImage) (rbc: RBC) (frame: Views.RBCFrame) =
         frame.Opacity <- if displayHealthy || rbc.setManually || rbc.infected then 1. else 0.
-        let color = if rbc.infected then colorRBCInfected else colorRBCHealthy
+        let color = if rbc.infected then srcImg.InfectedRBCColor else srcImg.HealthyRBCColor
         frame.manuallyAdded.Visibility <- if rbc.setManually then Visibility.Visible else Visibility.Hidden
         frame.manuallyAdded.Fill <- color
         frame.border.Stroke <- color
 
-    let RBCFrameFromExisting (rbc: RBC) (frame: Views.RBCFrame) : Views.RBCFrame =
+    let RBCFrameFromExisting (srcImg: SourceImage) (rbc: RBC) (frame: Views.RBCFrame) : Views.RBCFrame =
         frame.Visibility <- Visibility.Visible
         frame.Height <- rbc.size.Height
         frame.Width <- rbc.size.Width
         frame.Tag <- rbc
-        setRBCFrameStyle rbc frame
+        setRBCFrameStyle srcImg rbc frame
         frame.border.StrokeThickness <- 1.
         frame.txtRBCNumber.Text <- rbc.num.ToString()
         frame
 
+    let updateDocumentStatus () =
+        txtDocumentStatus.Text <- if state.FilePath = "" then "<New document>" else state.FilePath
+
+    let statusMessageTimer = Threading.DispatcherTimer()
+    statusMessageTimer.Tick.AddHandler(fun obj args -> statusMessageTimer.Stop(); txtMessageStatus.Text <- "")
+    statusMessageTimer.Interval <- TimeSpan(0, 0, 2)
+    let displayStatusMessage (message: string) =
+        txtMessageStatus.Text <- message
+        statusMessageTimer.Stop()
+        statusMessageTimer.Start()
+
     let highlightRBCFrame (frame: Views.RBCFrame) (highlight: bool) =
         let rbc = frame.Tag :?> RBC
         if highlight
@@ -101,33 +122,52 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
         scrollViewCurrentImage.ScrollToHorizontalOffset(rbc.center.X * currentScale - scrollViewCurrentImage.ViewportWidth / 2. + borderCurrentImage.BorderThickness.Left)
         scrollViewCurrentImage.ScrollToVerticalOffset(rbc.center.Y * currentScale - scrollViewCurrentImage.ViewportHeight / 2. + borderCurrentImage.BorderThickness.Top)
 
-    let parasitemiaText (nbTotal: int, nbInfected: int) : string =
-        if nbTotal = 0
-        then
-            ""
-        else
-            let percent = 100. * (float nbInfected) / (float nbTotal)
-            sprintf "%.1f %% (%d / %d)" percent nbInfected nbTotal
+
+    let txtImageName_TextChanged =
+        TextChangedEventHandler(fun obj args -> state.CurrentImage |> Option.iter(fun srcImg -> state.SetName srcImg txtImageName.Text))
 
     let updateCurrentImageInformation () =
+        txtImageName.TextChanged.RemoveHandler(txtImageName_TextChanged)
+        txtImageInformation1.Inlines.Clear()
+        txtImageInformation2.Inlines.Clear()
+        txtImageName.Text <- ""
+
         match state.CurrentImage with
         | Some srcImg ->
-            let parasitemiaStr = parasitemiaText (state.ImageParasitemia srcImg)
-            txtImageInformation.Inlines.Clear()
-            txtImageInformation.Inlines.Add(Documents.Run("Parasitemia: ", FontWeight = FontWeights.Bold))
-            txtImageInformation.Inlines.Add(parasitemiaStr)
-            txtImageInformation.Inlines.Add(Documents.LineBreak())
-
-            txtImageInformation.Inlines.Add(Documents.Run("Average erytrocyte diameter: ", FontWeight = FontWeights.Bold))
-            txtImageInformation.Inlines.Add(Documents.Run(srcImg.config.RBCRadius.ToString()))
-            txtImageInformation.Inlines.Add(Documents.LineBreak())
-
-            txtImageInformation.Inlines.Add(Documents.Run("Last analysis: ", FontWeight = FontWeights.Bold))
-            txtImageInformation.Inlines.Add(Documents.Run(if srcImg.dateLastAnalysis.Ticks = 0L then "<Never>" else srcImg.dateLastAnalysis.ToLocalTime().ToString()))
-        | _ -> ()
+            gridImageInformation.Visibility <- Visibility.Visible
+            txtImageName.Text <- srcImg.name
+            txtImageName.TextChanged.AddHandler(txtImageName_TextChanged)
+
+            // The left part.
+            let parasitemiaStr = Utils.percentText (state.ImageParasitemia srcImg)
+            txtImageInformation1.Inlines.Add(Documents.Run("Parasitemia: ", FontWeight = FontWeights.Bold))
+            txtImageInformation1.Inlines.Add(parasitemiaStr)
+            txtImageInformation1.Inlines.Add(Documents.LineBreak())
+
+            txtImageInformation1.Inlines.Add(Documents.Run("Last analysis: ", FontWeight = FontWeights.Bold))
+            txtImageInformation1.Inlines.Add(Documents.Run(if srcImg.dateLastAnalysis.Ticks = 0L then "<Never>" else srcImg.dateLastAnalysis.ToLocalTime().ToString()))
+
+            // The right part part.
+            txtImageInformation2.Inlines.Add(Documents.Run("Added infected erythrocyte: ", FontWeight = FontWeights.Bold))
+            txtImageInformation2.Inlines.Add(Documents.Run((state.ImageNbManuallyChangedRBCStr srcImg true) + " " + (state.ImageManuallyChangedRBCStr srcImg true)))
+            txtImageInformation2.Inlines.Add(Documents.LineBreak())
+            txtImageInformation2.Inlines.Add(Documents.Run("Removed infected erythrocyte: ", FontWeight = FontWeights.Bold))
+            txtImageInformation2.Inlines.Add(Documents.Run((state.ImageNbManuallyChangedRBCStr srcImg false) + " " + (state.ImageManuallyChangedRBCStr srcImg false)))
+
+        | _ ->
+            gridImageInformation.Visibility <- Visibility.Hidden
 
     let updateGlobalParasitemia () =
-        txtGlobalParasitemia.Text <- parasitemiaText state.GlobalParasitemia
+        txtGlobalParasitemia.Inlines.Clear()
+        let total, infected = state.GlobalParasitemia
+        txtGlobalParasitemia.Inlines.Add(Documents.Run(Utils.percentText (total, infected), FontWeight = FontWeights.Bold))
+        if total > 0 && total < warningBelowNumberOfRBC
+        then
+            txtGlobalParasitemia.Inlines.Add(
+                Documents.Run(
+                    sprintf " Warning: the number of erythrocytes should be above %d" warningBelowNumberOfRBC,
+                    FontWeight = FontWeights.Bold,
+                    Foreground = Brushes.Red))
 
     let updateViewportPreview () =
         for preview in stackPreviews.Children |> Seq.cast<Views.ImageSourcePreview> do
@@ -154,7 +194,7 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
             else
                 preview.viewport.Visibility <- Visibility.Hidden
 
-    let rec setAsInfected (rbc: RBC) (infected: bool) =
+    let rec setAsInfected (srcImg: SourceImage) (rbc: RBC) (infected: bool) =
         state.SetAsInfected rbc infected
         canvasCurrentImage.Children
         |> Seq.cast<Views.RBCFrame>
@@ -162,16 +202,16 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
             (fun frame ->
                 if (frame.Tag :?> RBC) = rbc
                 then
-                    setRBCFrameStyle rbc frame)
+                    setRBCFrameStyle srcImg rbc frame)
         updateRBCFramesPreview ()
         updateCurrentImageInformation ()
         updateGlobalParasitemia ()
 
-    and RBCFrame (rbc: RBC) : Views.RBCFrame =
-        let frame = RBCFrameFromExisting rbc (Views.RBCFrame())
+    and RBCFrame (srcImg: SourceImage) (rbc: RBC) : Views.RBCFrame =
+        let frame = RBCFrameFromExisting srcImg rbc (Views.RBCFrame())
         frame.SetValue(Panel.ZIndexProperty, Int32.MaxValue - rbc.num) // To be sure the
-        frame.menuRBCSetAsHealthy.Click.AddHandler(fun obj args -> setAsInfected (frame.Tag :?> RBC) false)
-        frame.menuRBCSetAsInfected.Click.AddHandler(fun obj args -> setAsInfected (frame.Tag :?> RBC) true)
+        frame.menuRBCSetAsHealthy.Click.AddHandler(fun obj args -> setAsInfected srcImg (frame.Tag :?> RBC) false)
+        frame.menuRBCSetAsInfected.Click.AddHandler(fun obj args -> setAsInfected srcImg (frame.Tag :?> RBC) true)
         frame.ContextMenuOpening.AddHandler(
             fun obj args ->
                 if (frame.Tag :?> RBC).infected
@@ -195,9 +235,9 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
                 let previewInfected =
                     if currentPreview < stackRBC.Children.Count
                     then
-                        RBCFrameFromExisting rbc (stackRBC.Children.[currentPreview] :?> Views.RBCFrame)
+                        RBCFrameFromExisting srcImg rbc (stackRBC.Children.[currentPreview] :?> Views.RBCFrame)
                     else
-                        let f = RBCFrame rbc
+                        let f = RBCFrame srcImg rbc
                         f.MouseLeftButtonUp.AddHandler(fun obj args -> zoomToRBC (f.Tag :?> RBC))
                         stackRBC.Children.Add(f) |> ignore
                         f
@@ -221,9 +261,9 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
                 let frame =
                     if currentCanvas < canvasCurrentImage.Children.Count
                     then
-                        RBCFrameFromExisting rbc (canvasCurrentImage.Children.[currentCanvas] :?> Views.RBCFrame)
+                        RBCFrameFromExisting srcImg rbc (canvasCurrentImage.Children.[currentCanvas] :?> Views.RBCFrame)
                     else
-                        let f = RBCFrame rbc
+                        let f = RBCFrame srcImg rbc
                         f.Root.Opacity <- 0.7
                         canvasCurrentImage.Children.Add(f) |> ignore
                         f
@@ -237,23 +277,47 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
                 canvasCurrentImage.Children.[i].Visibility <- Visibility.Hidden
         | _ -> ()
 
+    let askDocumentPathToSave () : string option =
+        let dialog = SaveFileDialog(AddExtension = true, DefaultExt = PiaZ.extension, Filter = PiaZ.filter)
+
+        if state.FilePath <> ""
+        then
+            dialog.FileName <- FileInfo(state.FilePath).Name
+        elif state.PatientID <> ""
+        then
+            dialog.FileName <- state.PatientID + PiaZ.extension
+
+        let res = dialog.ShowDialog()
+        if res.HasValue && res.Value then
+            Some dialog.FileName
+        else
+            None
+
     let saveCurrentDocument () =
         try
             if state.FilePath = ""
             then
-                let dialog = SaveFileDialog(AddExtension = true, DefaultExt = PiaZ.extension, Filter = PiaZ.filter);
-                let res = dialog.ShowDialog()
-                if res.HasValue && res.Value
-                then
-                    state.FilePath <- dialog.FileName
+                match askDocumentPathToSave () with
+                | Some filepath ->
+                    state.FilePath <- filepath
                     state.Save()
+                | _ -> ()
             else
                 state.Save()
+            updateDocumentStatus ()
+            displayStatusMessage "Document saved"
         with
         | :? IOException as ex ->
             Log.Error(ex.ToString())
             MessageBox.Show(sprintf "The document cannot be save in '%s'" state.FilePath, "Error saving the document", MessageBoxButton.OK, MessageBoxImage.Error) |> ignore
 
+    let saveCurrentDocumentAsNewFile () =
+        match askDocumentPathToSave () with
+        | Some filepath ->
+            state.FilePath <- filepath
+            saveCurrentDocument ()
+        | _ -> ()
+
     // Ask the use to save the current document if neccessary.
     let askSaveCurrent () =
         if state.AlteredSinceLastSave
@@ -265,6 +329,8 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
     let updateCurrentImage () =
         match state.CurrentImage with
         | Some srcImg ->
+            imgLogos.Visibility <- Visibility.Collapsed
+
             // Highlight the preview.
             stackPreviews.Children
             |> Seq.cast<Views.ImageSourcePreview>
@@ -276,11 +342,15 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
 
             updateRBCFramesCurrent ()
             updateRBCFramesPreview ()
-            updateCurrentImageInformation ()
+
         | None ->
+            imgLogos.Visibility <- Visibility.Visible
+
             stackRBC.Children.Clear()
             canvasCurrentImage.Children.Clear()
-            canvasCurrentImage.Background <- Brushes.Black
+            canvasCurrentImage.Background <- canvasCurrentImageColor
+
+        updateCurrentImageInformation ()
 
     let setCurrentImage (srcImg: SourceImage) =
         if state.CurrentImage.IsNone || state.CurrentImage.Value <> srcImg
@@ -299,6 +369,10 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
             if currentRemoved
             then
                 updateCurrentImage()
+
+            updateGlobalParasitemia()
+
+            // Update image numbers.
             stackPreviews.Children |> Seq.cast<Views.ImageSourcePreview> |> Seq.iter (fun imgPreview -> imgPreview.txtImageNumber.Text <- (imgPreview.Tag :?> SourceImage).num.ToString()))
 
         imgCtrl.Tag <- srcImg
@@ -343,6 +417,7 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
         txtPatient.Text <- state.PatientID
         updatePreviews ()
         updateGlobalParasitemia ()
+        updateDocumentStatus ()
 
     let loadFile (filepath: string) =
         askSaveCurrent ()
@@ -355,27 +430,47 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
         | :? IOException as ex ->
             Log.Error(ex.ToString())
             state.FilePath <- previousFilePath
-            MessageBox.Show(sprintf "The document cannot be loaded from '%s'" state.FilePath, "Error saving the document", MessageBoxButton.OK, MessageBoxImage.Error) |> ignore
-
-    txtPatient.LostFocus.AddHandler(fun obj args -> state.PatientID <- txtPatient.Text)
-
-    menuExit.Click.AddHandler(fun obj args ->
-        askSaveCurrent ()
-        mainWindow.Root.Close())
-
-    menuSaveFile.Click.AddHandler(fun obj args -> saveCurrentDocument ())
+            MessageBox.Show(sprintf "The document cannot be loaded from '%s'" state.FilePath, "Error loading the document", MessageBoxButton.OK, MessageBoxImage.Error) |> ignore
 
-    menuLoadFile.Click.AddHandler(fun obj args ->
-        // TODO: if current state not saved and not empty, ask to save it.
+    let askLoadFile () =
         let dialog = OpenFileDialog(Filter = PiaZ.filter)
         let res = dialog.ShowDialog()
         if res.HasValue && res.Value
-        then loadFile dialog.FileName)
+        then loadFile dialog.FileName
 
-    menuNewFile.Click.AddHandler(fun obj args ->
+    let newFile () =
         askSaveCurrent ()
         state.Reset()
-        updateGUI())
+        updateGUI()
+
+    let exportResults () =
+        let extension = ".txt"
+        let dialog = SaveFileDialog(AddExtension = true, DefaultExt = extension)
+
+        if state.FilePath <> ""
+        then
+            dialog.FileName <- Path.GetFileNameWithoutExtension(state.FilePath) + extension
+        elif state.PatientID <> ""
+        then
+            dialog.FileName <- state.PatientID + extension
+
+        let res = dialog.ShowDialog()
+        if res.HasValue && res.Value then
+            try
+                Export.exportResults state dialog.FileName
+            with
+            | :? IOException as ex ->
+                Log.Error(ex.ToString())
+                MessageBox.Show(sprintf "The results cannot be exported in '%s'" state.FilePath, "Error exporting the files", MessageBoxButton.OK, MessageBoxImage.Error) |> ignore
+
+    txtPatient.TextChanged.AddHandler(fun obj args -> state.PatientID <- txtPatient.Text)
+
+    menuExit.Click.AddHandler(fun obj args -> mainWindow.Root.Close())
+    menuSaveFile.Click.AddHandler(fun obj args -> saveCurrentDocument ())
+    menuSaveAsFile.Click.AddHandler(fun obj args -> saveCurrentDocumentAsNewFile ())
+    menuLoadFile.Click.AddHandler(fun obj args -> askLoadFile ())
+    menuNewFile.Click.AddHandler(fun obj args -> newFile ())
+    menuExportResults.Click.AddHandler(fun obj args -> exportResults ())
 
     menuAddSourceImage.Click.AddHandler(fun obj args ->
         let dialog = OpenFileDialog(Filter = "Image Files|*.png;*.jpg;*.tif;*.tiff", Multiselect = true)
@@ -385,8 +480,13 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
             let noSourceImage = state.SourceImages.Count() = 0
 
             for filename in dialog.FileNames do
-                let srcImg = state.AddSourceImage filename defaultConfig
-                addPreview srcImg
+                try
+                    let srcImg = state.AddSourceImage filename defaultConfig
+                    addPreview srcImg
+                with
+                | _ as ex ->
+                    Log.Error(ex.ToString())
+                    MessageBox.Show(sprintf "Unable to read the image from '%s'" filename, "Error adding an image", MessageBoxButton.OK, MessageBoxImage.Error) |> ignore
 
             updateGlobalParasitemia ()
 
@@ -409,6 +509,8 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
 
     menuAbout.Click.AddHandler(fun obj args -> About.showWindow mainWindow.Root)
 
+    mainWindow.Root.Closing.AddHandler(fun obj args -> askSaveCurrent ())
+
     // Zoom on the current image.
     let adjustCurrentImageBorders (deltaX: float) (deltaY: float) =
         borderCurrentImage.BorderThickness <-
@@ -491,13 +593,47 @@ let run (defaultConfig: Config) (fileToOpen: string option) =
             borderCurrentImage.ReleaseMouseCapture()
             args.Handled <- true)
 
+    // Shortcuts.
+    // Save.
+    mainWindow.Root.InputBindings.Add(
+        Input.KeyBinding(
+            FSharp.ViewModule.FunCommand((fun obj -> saveCurrentDocument ()), (fun obj -> true)),
+            Input.KeyGesture(Input.Key.S, Input.ModifierKeys.Control))) |> ignore
+
+    // Save as.
+    mainWindow.Root.InputBindings.Add(
+        Input.KeyBinding(
+            FSharp.ViewModule.FunCommand((fun obj -> saveCurrentDocumentAsNewFile ()), (fun obj -> true)),
+            Input.KeyGesture(Input.Key.S, Input.ModifierKeys.Control ||| Input.ModifierKeys.Shift))) |> ignore
+
+    // Open.
+    mainWindow.Root.InputBindings.Add(
+        Input.KeyBinding(
+            FSharp.ViewModule.FunCommand((fun obj -> askLoadFile ()), (fun obj -> true)),
+            Input.KeyGesture(Input.Key.O, Input.ModifierKeys.Control))) |> ignore
+
+    // New file.
+    mainWindow.Root.InputBindings.Add(
+        Input.KeyBinding(
+            FSharp.ViewModule.FunCommand((fun obj -> newFile ()), (fun obj -> true)),
+            Input.KeyGesture(Input.Key.N, Input.ModifierKeys.Control))) |> ignore
+
+    // Export results.
+    mainWindow.Root.InputBindings.Add(
+        Input.KeyBinding(
+            FSharp.ViewModule.FunCommand((fun obj -> exportResults ()), (fun obj -> true)),
+            Input.KeyGesture(Input.Key.E, Input.ModifierKeys.Control))) |> ignore
+
     // Viewport preview.
     scrollViewCurrentImage.ScrollChanged.AddHandler(fun obj args -> updateViewportPreview ())
 
+    updateDocumentStatus ()
+    gridImageInformation.Visibility <- Visibility.Hidden
+
     mainWindow.Root.Show()
 
     match fileToOpen with
     | Some filepath -> loadFile filepath
     | None -> ()
 
-    app.Run()
\ No newline at end of file
+    app.Run()