/// Copyright (c) 2021 Iiro Iivanainen, Harri Linna, Jere Pakkanen, Riikka Vilavaara /// /// Permission is hereby granted, free of charge, to any person obtaining /// a copy of this software and associated documentation files (the /// "Software"), to deal in the Software without restriction, including /// without limitation the rights to use, copy, modify, merge, publish, /// distribute, sublicense, and/or sell copies of the Software, and to /// permit persons to whom the Software is furnished to do so, subject to /// the following conditions: /// /// The above copyright notice and this permission notice shall be included /// in all copies or substantial portions of the Software. /// /// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, /// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF /// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. /// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY /// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, /// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE /// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. /// using System; using System.Collections.Generic; using System.Threading.Tasks; using CoreLibrary; using System.IO; using System.Windows.Media.Imaging; using System.Windows.Media; namespace GroundhogApp { /// /// View Model class for the MainWindow /// class MainWindowViewModel : ViewModelBase { private Sample sample; public bool CanReadImage { get; set; } /// /// MainWindowViewModel constructor /// public MainWindowViewModel() { XAxisColor = Colors.Red; YAxisColor = Colors.Blue; ZAxisColor = Colors.Green; DataOnMapColor = Colors.Red; DataOnMapHighlightColor = Colors.Orange; UserName = Utilities.GetEnvironmentUser(); NewSample(); } /// /// Settings when creating new sample. /// internal void NewSample() { CanReadImage = false; sample = new(); ShowAxes = false; ShowDataOnMap = true; LevelMin = 0.0; LevelMax = 100.0; XZDepth = 1; ZYDepth = 1; XYDepth = 1; XYexists = false; XZexists = false; ZYexists = false; XYdatalist = null; ZYdatalist = null; XZdatalist = null; XYsource = null; ZYsource = null; XZsource = null; EnableUI = false; UpdateMarkCount(); IsSaved = true; } private int[] _XYMarkAmounts; /// /// How many marks are associated with each XY-slice. /// public int[] XYMarkAmounts { get { return _XYMarkAmounts; } set { _XYMarkAmounts = value; RaisePropertyChanged(nameof(XYMarkAmounts)); } } private int[] _ZYMarkAmounts; /// /// How many marks are associated with each ZY-slice. /// public int[] ZYMarkAmounts { get { return _ZYMarkAmounts; } set { _ZYMarkAmounts = value; RaisePropertyChanged(nameof(ZYMarkAmounts)); } } private int[] _XZMarkAmounts; /// /// How many marks are associated with each XZ-slice. /// public int[] XZMarkAmounts { get { return _XZMarkAmounts; } set { _XZMarkAmounts = value; RaisePropertyChanged(nameof(XZMarkAmounts)); } } private int _XYDepth; /// /// Which slice is being shown from the XY-plane. /// public int XYDepth { get { return _XYDepth; } set { _XYDepth = value; RaisePropertyChanged(nameof(XYDepth)); UpdateStackZ(); } } private int _ZYdepth; /// /// Which slice is being shown from the YZ-plane. /// public int ZYDepth { get { return _ZYdepth; } set { _ZYdepth = value; RaisePropertyChanged(nameof(ZYDepth)); UpdateStackX(); } } private int _XZDepth; /// /// Which slice is being shown from the XZ-plane. /// public int XZDepth { get { return _XZDepth; } set { _XZDepth = value; RaisePropertyChanged(nameof(XZDepth)); UpdateStackY(); } } private BitmapImage _XYsource; /// /// The image that is shown from XY plane. /// public BitmapImage XYsource { get { return _XYsource; } set { _XYsource = value; RaisePropertyChanged(nameof(XYsource)); } } private BitmapImage _ZYsource; /// /// The image that is shown from ZY plane. /// public BitmapImage ZYsource { get { return _ZYsource; } set { _ZYsource = value; RaisePropertyChanged(nameof(ZYsource)); } } private BitmapImage _XZsource; /// /// The image that is shown from XZ plane. /// public BitmapImage XZsource { get { return _XZsource; } set { _XZsource = value; RaisePropertyChanged(nameof(XZsource)); } } private List _XYdatalist; /// /// List of attached data on currently shown XY slice. /// public List XYdatalist { get { return _XYdatalist; } set { _XYdatalist = value; RaisePropertyChanged(nameof(XYdatalist)); } } private List _ZYdatalist; /// /// List of attached data on currently shown ZY slice. /// public List ZYdatalist { get { return _ZYdatalist; } set { _ZYdatalist = value; RaisePropertyChanged(nameof(ZYdatalist)); } } private List _XZdatalist; /// /// List of attached data on currently shown XZ slice. /// public List XZdatalist { get { return _XZdatalist; } set { _XZdatalist = value; RaisePropertyChanged(nameof(XZdatalist)); } } private double _LevelMin; /// /// Image level minimum in percentage. /// public double LevelMin { get { return _LevelMin; } set { _LevelMin = value; RaisePropertyChanged(nameof(LevelMin)); AdjustLevel(); } } private double _LevelMax; /// /// Image level maximum in percentage. /// public double LevelMax { get { return _LevelMax; } set { _LevelMax = value; RaisePropertyChanged(nameof(LevelMax)); AdjustLevel(); } } private bool _XYexists; /// /// If XY map exists or not. Used for enabling UI elements. /// public bool XYexists { get { return _XYexists; } set { _XYexists = value; RaisePropertyChanged(nameof(XYexists)); } } private bool _ZYexists; /// /// If ZY map exists or not. Used for enabling UI elements. /// public bool ZYexists { get { return _ZYexists; } set { _ZYexists = value; RaisePropertyChanged(nameof(ZYexists)); } } private bool _XZexists; /// /// If XY map exists or not. Used for enabling UI elements. /// public bool XZexists { get { return _XZexists; } set { _XZexists = value; RaisePropertyChanged(nameof(XZexists)); } } #region Color settings private Color _XAxisColor; /// /// Color of the X axis. /// public Color XAxisColor { get { return _XAxisColor; } set { _XAxisColor = value; RaisePropertyChanged(nameof(XAxisColor)); } } private Color _YAxisColor; /// /// Color of the Y axis. /// public Color YAxisColor { get { return _YAxisColor; } set { _YAxisColor = value; RaisePropertyChanged(nameof(YAxisColor)); } } private Color _ZAxisColor; /// /// Color of the Z axis. /// public Color ZAxisColor { get { return _ZAxisColor; } set { _ZAxisColor = value; RaisePropertyChanged(nameof(ZAxisColor)); } } private Color _DataOnMapColor; /// /// Color of Attached data points shown on map. /// public Color DataOnMapColor { get { return _DataOnMapColor; } set { _DataOnMapColor = value; RaisePropertyChanged(nameof(DataOnMapColor)); } } private Color _DataOnMapHighlightColor; /// /// Color of attached data point shown on map /// when mouse hovers over them. /// public Color DataOnMapHighlightColor { get { return _DataOnMapHighlightColor; } set { _DataOnMapHighlightColor = value; RaisePropertyChanged(nameof(DataOnMapHighlightColor)); } } #endregion #region Viewing settings private bool _ShowAxes; /// /// Wether color-coded axes are shown or not. /// public bool ShowAxes { get { return _ShowAxes; } set { _ShowAxes = value; RaisePropertyChanged(nameof(ShowAxes)); } } private bool _ShowDataOnMap; /// /// Are Attached data points shown on Map or not. /// public bool ShowDataOnMap { get { return _ShowDataOnMap; } set { _ShowDataOnMap = value; RaisePropertyChanged(nameof(ShowDataOnMap)); } } #endregion /// /// Are changed saved or not. /// public bool IsSaved { get; set; } /// /// Bit depth of the Map. /// public int BitDepth { get; set; } /// /// Saves already saved sample to the same filepath. /// internal void SaveSample() { if (!(sample.Filepath == null)) { sample.SetSampleRelations(sample.Filepath); sample.MetaList.SetData("Last edited", Utilities.GetDateTime()); FileManager.SerializeSync(this.sample, sample.Filepath); IsSaved = true; } } /// /// Save sample as a JSON file. /// /// internal void SaveAsSample(string filepath) { sample.Filepath = filepath; sample.SetSampleRelations(filepath); sample.MetaList.SetData("Created by", UserName); sample.MetaList.SetData("Time created", Utilities.GetDateTime()); FileManager.SerializeSync(this.sample, sample.Filepath); IsSaved = true; } /// /// Checks if save file path already exists or not. /// /// True if sample has a filepath saved. public bool SavePathExists() { if (GetSampleFilePath() != null) return true; return false; } /// /// Loads sample from JSON file. /// /// File path of sample. internal void LoadSample(string filepath) { sample = FileManager.Dezerialize(filepath); sample.Filepath = filepath; sample.SetSampleRelations(filepath); if (sample.GetFilepathZ() != null) { CanReadImage = true; XYDepth = 1; XYexists = true; UpdateStackZ(); EnableUI = true; UpdateMarkCount(); BitDepth = sample.GetBitDepth(); } else { CanReadImage = false; XYexists = false; XYDepth = 1; EnableUI = false; } if (sample.GetFilepathX() != null) { ZYDepth = 1; ZYexists = true; UpdateStackX(); UpdateMarkCount(); } else { ZYexists = false; ZYDepth = 1; } if (sample.GetFilepathY() != null) { XZDepth = 1; XZexists = true; UpdateStackY(); UpdateMarkCount(); } else { XZexists = false; XZDepth = 1; } IsSaved = true; } /// /// Creates X stack from the Z stack of the sample and write it /// into the harddrive. /// /// Filepath where new stack is written /// Task that writes new stack to harddrive internal async Task CreateXStack(string filepath) { try { int nameIndex = filepath.LastIndexOf("\\"); string newStackPath = filepath.Substring(0, nameIndex); string newStackName = filepath.Substring(nameIndex + 1, filepath.Length - nameIndex - 1); string zPath = sample.Map.StackZ.Filepath.GetPath(); string zTemplate = sample.Map.StackZ.Template; int zHeight = sample.Map.GetZHeigth(); int zWidth = sample.Map.GetZWidth(); int zDepth = sample.Map.GetZDepth(); int zByteDepth = sample.Map.ByteDepth; switch (sample.Map.MapType) { case Map.FileType.ImageSequence: await Task.Run(() => MapReader.MakeSeqXStack(zPath, zTemplate, newStackPath, newStackName)); break; case Map.FileType.Tiff3D: break; case Map.FileType.Raw: await Task.Run(() => MapReader.MakeRawXStack(zPath, newStackPath, newStackName, zHeight, zWidth, zDepth, zByteDepth)); break; default: break; } sample.SetMapTemplateX(newStackName); string filetype = sample.GetMapFileType(); sample.SetFilepathX(Path.ChangeExtension(filepath, null) + filetype); ZYexists = true; UpdateStackX(); } catch(StackWriteException) { sample.Map.StackX = new Slice(); throw; } finally { IsSaved = false; } } /// /// Creates Y stack from the Z stack of the sample and write it /// into the harddrive. /// /// Filepath to where new stack is written /// Task that writes new stack to harddrive internal async Task CreateYStack(string filepath) { try { int nameIndex = filepath.LastIndexOf("\\"); string newStackPath = filepath.Substring(0, nameIndex); string newStackName = filepath.Substring(nameIndex + 1, filepath.Length - nameIndex - 1); string zPath = sample.Map.StackZ.Filepath.GetPath(); string zTemplate = sample.Map.StackZ.Template; int zHeight = sample.Map.GetZHeigth(); int zWidth = sample.Map.GetZWidth(); int zDepth = sample.Map.GetZDepth(); int zByteDepth = sample.Map.ByteDepth; switch (sample.Map.MapType) { case Map.FileType.ImageSequence: await Task.Run(() => MapReader.MakeSeqYStack(zPath, zTemplate, newStackPath, newStackName)); break; case Map.FileType.Tiff3D: break; case Map.FileType.Raw: await Task.Run(() => MapReader.MakeRawYStack(zPath, newStackPath, newStackName, zHeight, zWidth, zDepth, zByteDepth)); break; default: break; } sample.SetMapTemplateY(newStackName); string filetype = sample.GetMapFileType(); sample.SetFilepathY(Path.ChangeExtension(filepath, null) + filetype); XZexists = true; UpdateStackY(); } catch(StackWriteException) { sample.Map.StackY = new Slice(); throw; } finally { IsSaved = false; } } /// /// Check to see if there are generated stacks. /// /// True if there are generated stacks, false otherwise internal bool GeneratedStacks() { return ZYexists || XZexists; } /// /// If Z stack is generated, returns its filename. /// /// Filename of the z stack or "" if stack not specified public string GetStackZName() { if (sample.GetFilepathZ() != null) { if(sample.Map.MapType == Map.FileType.ImageSequence) return Path.GetFileNameWithoutExtension(sample.GetFilepathZ()).TrimEnd( new[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }); return Path.GetFileNameWithoutExtension(sample.GetFilepathZ()); } return ""; } /// /// Returns folder where Z stack is saved /// /// Folder of the Z stack public string GetStackZPath() { if (sample.GetFilepathZ() != null) return Path.GetDirectoryName(sample.GetFilepathZ()); return ""; } /// /// Returns the info if the map file is sequence /// /// True if map is sequence, false otherwise public bool IsSequence() { return sample.Map.MapType == Map.FileType.ImageSequence; } /// /// Returns the info if the map file is 3Dtiff /// /// True if map is 3Dtiff, false otherwise public bool Is3DTiff() { return sample.Map.MapType == Map.FileType.Tiff3D; } /// /// Returns the info if the map file is raw /// /// True if map is raw, false otherwise public bool IsRaw() { return sample.Map.MapType == Map.FileType.Raw; } /// /// Delete stacks and if sample is saved, save the sample /// after removing added stacks. /// internal void DeleteGeneratedStacks() { // Shouldn't access Slice filepaths directly // from here. Map's and Slice's attributes have // too high publicity. if (sample.Map.StackX.Filepath != null) { switch (sample.Map.MapType) { case Map.FileType.ImageSequence: MapReader.DeleteStack(sample.Map.StackX.Filepath.GetPath(), sample.Map.StackX.Template, true); break; case Map.FileType.Tiff3D: MapReader.DeleteStack(sample.Map.StackX.Filepath.GetPath(), "", false); break; case Map.FileType.Raw: MapReader.DeleteStack(sample.Map.StackX.Filepath.GetPath(), "", false); break; default: break; } sample.Map.StackX.Filepath = null; sample.Map.StackX.Template = null; } if (sample.Map.StackY.Filepath != null) { switch (sample.Map.MapType) { case Map.FileType.ImageSequence: MapReader.DeleteStack(sample.Map.StackY.Filepath.GetPath(), sample.Map.StackY.Template, true); break; case Map.FileType.Tiff3D: MapReader.DeleteStack(sample.Map.StackY.Filepath.GetPath(), "", false); break; case Map.FileType.Raw: MapReader.DeleteStack(sample.Map.StackY.Filepath.GetPath(), "", false); break; default: break; } sample.Map.StackY.Filepath = null; sample.Map.StackY.Template = null; } if (IsSaved) SaveSample(); } private bool _EnableUI; /// /// If specific UI elements are enabled or not. /// public bool EnableUI { get { return _EnableUI; } set { _EnableUI = value; RaisePropertyChanged(nameof(EnableUI)); } } /// /// List of attached data points. /// public List AttData { get { return sample.AttDataList.AttachmentList; } } private string _UserName; public string UserName { get { return _UserName; } set { _UserName = value; RaisePropertyChanged(nameof(UserName)); } } /// /// Setting map as Image Sequence. /// /// File path /// Image template internal void SetImageSeq(string path, string template) { sample.SetImageSequence(); sample.SetMapTemplateZ(template); SetMap(path); } /// /// Setting Map as multiframe tiff. /// /// File path internal void SetTiff3D(string path) { sample.SetTiff3D(); SetMap(path); } /// /// Setting Map as Raw file. /// /// File path. /// Byte depth. /// Width of Map. /// Height of Map. /// True if little endian, false if big /// endian. internal void SetRaw(string path, int byteDepth, int mapWidth, int mapHeight, bool? littleEndian) { sample.SetRaw(); sample.SetByteDepth(byteDepth); sample.SetMapWidth(mapWidth); sample.SetMapHeight(mapHeight); sample.SetLittleEndian((bool)littleEndian); SetMap(path); } /// /// Settings for Map. /// /// File path. private void SetMap(string path) { sample.SetFilepathZ(path); sample.SetSlicesZ(path); EnableUI = true; CanReadImage = true; XYDepth = 1; XYexists = true; UpdateStackZ(); sample.SetMapWidth((int)XYsource.Width); sample.SetMapHeight((int)XYsource.Height); UpdateMarkCount(); BitDepth = sample.GetBitDepth(); IsSaved = false; } /// /// Adjust levels of slices by percentage. /// private void AdjustLevel() { if (!CanReadImage) return; if (XYsource != null) { sample.AdjustLevelZ(LevelMin, LevelMax); ConvertStack(x => XYsource = x, () => sample.GetSliceZ()); } if (ZYsource != null) { sample.AdjustLevelX(LevelMin, LevelMax); ConvertStack(x => ZYsource = x, () => sample.GetSliceX()); } if (XZsource != null) { sample.AdjustLevelY(LevelMin, LevelMax); ConvertStack(x => XZsource = x, () => sample.GetSliceY()); } } /// /// Adds attached data with specified values and adds default metadata. /// /// File destination of the attached data. /// Given name for the attached data. /// Metadata description. /// Coordinates given for the attached data. /// If the data be copied to sample folder. public void AddAttData(string filePath, string dataName, string dataDescription, List> coordinates, bool copyToFolder) { var copyPath = GetSampleSaveFolder(); if (copyToFolder) { var folderName = Path.GetFileNameWithoutExtension(GetSampleFilePath()) + "_AttachedData"; filePath = Utilities.CopyToFolder(filePath, folderName, copyPath); } var ad = new AttachedData(filePath, dataName, coordinates); string username = UserName; string dateTime = Utilities.GetDateTime(); ad.SetMetaData("Description", dataDescription); ad.SetMetaData("Added by", username); ad.SetMetaData("Time added", dateTime); sample.AttDataList.AttachmentList.Add(ad); UpdateMarkCount(); UpdateDataOnMap(); IsSaved = false; } /// /// Applies Metadata list of edited Metas-object to the target Metas-object. /// /// Target metas object. /// New metalist. public void ApplyMetadataChanges(Metas target, Metas edited) { target.MetaList = edited.MetaList; IsSaved = false; } /// /// Update the datalists that contain attached data /// on current slices. /// private void UpdateDataOnMap() { XYdatalist = sample.AttachmentsByZCoordinate(XYDepth); ZYdatalist = sample.AttachmentsByXCoordinate(ZYDepth); XZdatalist = sample.AttachmentsByYCoordinate(XZDepth); } /// /// Update the mark counts for the MarkedSlider. /// private void UpdateMarkCount() { XYMarkAmounts = sample.GetMarkCount(2); // Z XZMarkAmounts = sample.GetMarkCount(1); // Y ZYMarkAmounts = sample.GetMarkCount(0); // X } #region ListViewFiltering public string CollectionFilterText { get; set; } public bool CollectionFilter(object item) { if (string.IsNullOrEmpty(CollectionFilterText)) return true; else return (item as AttachedData).DataName.Contains(CollectionFilterText, StringComparison.OrdinalIgnoreCase); } #endregion /// /// Removes selected attached data from the sample. /// /// public void DeleteAttData(object connector) { var attData = connector as AttachedData; sample.AttDataList.RemoveData(attData); UpdateMarkCount(); UpdateDataOnMap(); IsSaved = false; } /// /// Reading the image to be shown as stack Z. /// private void UpdateStackZ() { if (!CanReadImage) return; if (!XYexists) return; sample.UpdateStackZ(XYDepth - 1); sample.AdjustLevelZ(LevelMin, LevelMax); ConvertStack(x => XYsource = x, () => sample.GetSliceZ()); } /// /// Reading the image to be shown as stack X. /// private void UpdateStackX() { if (!CanReadImage) return; if (!ZYexists) return; sample.UpdateStackX(ZYDepth - 1); ConvertStack(x => ZYsource = x, () => sample.GetSliceX()); } /// /// Reading the image to be shown as stack Y. /// private void UpdateStackY() { if (!CanReadImage) return; if (!XZexists) return; sample.UpdateStackY(XZDepth - 1); ConvertStack(x => XZsource = x, () => sample.GetSliceY()); } /// /// Goes to the first coordinate point of the connector. /// If the some slices are out of bounds then they are not set. /// /// Connector given public void GoToConnector(AttachedData connector) { var points = connector.AttachmentPoints; if (points.Count < 1) return; var point = points[0]; int x = (int)point.X; int y = (int)point.Y; int z = (int)point.Z; if (XYexists && XYMarkAmounts.Length >= z) XYDepth = z; if (ZYexists && ZYMarkAmounts.Length >= x) ZYDepth = x; if (XZexists && XZMarkAmounts.Length >= y) XZDepth = y; } /// /// Updating specified stack and displaying it. /// /// Which source image attribute is /// changed. /// Which slice should be retrieved from /// Sample. private void ConvertStack( Action setSourceDelegate, Func getSliceDelegate) { if (!CanReadImage) return; byte[] bytes = getSliceDelegate(); if (bytes != null) { var image = new BitmapImage(); using (var mem = new MemoryStream(bytes)) { mem.Position = 0; image.BeginInit(); image.CreateOptions = BitmapCreateOptions.PreservePixelFormat; image.CacheOption = BitmapCacheOption.OnLoad; image.UriSource = null; image.StreamSource = mem; image.EndInit(); } setSourceDelegate(image); } else setSourceDelegate(null); UpdateDataOnMap(); } /// /// Gets the metas associated with the Sample. /// /// Metas associated with the Sample public Metas GetSampleMetaData() { return sample.MetaList; } /// /// Get sample save folder path /// /// Path to sample save folder /// /// Gets the save folder for the sample. /// /// Save folder for the sample public string GetSampleSaveFolder() { return Path.GetDirectoryName(sample.GetFilepath()); } /// /// Gets the filepath of the sample. /// /// filepath where the sample is saved public string GetSampleFilePath() { return sample.GetFilepath(); } /// /// Searching attachments by its name. /// /// Name to be searched. /// AttachedData with specified name. internal AttachedData GetAttachmentByName(string name) { return sample.GetAttachment(name); } /// /// Removes X stack related things. /// internal void CleanZYStack() { ZYsource = null; ZYexists = false; sample.CleanXStack(); } /// /// Removes Y stack related things. /// internal void CleanXZStack() { XZsource = null; XZexists = false; sample.CleanYStack(); } } }