From 706ae03ba548a4e5696d959050d6476828dde979 Mon Sep 17 00:00:00 2001 From: Nick Saw Date: Wed, 7 Dec 2022 00:47:55 +0000 Subject: [PATCH] Merged PR 69077: December 2022 release --- Directory.Build.props | 2 +- Psi.sln | 7 - .../CalibrationExtensions.cs | 116 +- .../CameraIntrinsics.cs | 7 + .../ICameraIntrinsics.cs | 4 + .../Annotations/Operators.cs | 141 + .../BatchProcessingTaskConfiguration.cs | 50 + .../BatchProcessingTaskMetadata.cs | 20 +- Sources/Data/Microsoft.Psi.Data/Dataset.cs | 30 +- .../Helpers/IndexHelper.cs | 19 +- .../Helpers/NearestType.cs} | 4 +- Sources/Data/Microsoft.Psi.Data/IPartition.cs | 12 +- .../Partition{TStreamReader}.cs | 14 +- Sources/Data/Microsoft.Psi.Data/Session.cs | 39 +- Sources/Data/Test.Psi.Data/DatasetTests.cs | 38 +- .../ImageToGZipStreamEncoder.cs | 29 - .../Microsoft.Psi.Imaging/DepthImage.cs | 22 +- .../Imaging/Microsoft.Psi.Imaging/Image.cs | 19 +- .../Microsoft.Psi.Imaging/ImageBase.cs | 50 +- .../Microsoft.Psi.Imaging/ImagePool.cs | 25 +- .../ImageToGzipStreamEncoder.cs | 0 .../Microsoft.Psi.Imaging.csproj | 2 +- .../Test.Psi.Imaging.Windows/ImageTester.cs | 256 +- .../Properties/AssemblyInfo.cs | 15 - .../Test.Psi.Imaging.Windows.csproj | 236 +- ....CognitiveServices.Language.Windows.csproj | 1 + .../ImageNet/ImageNetModelRunner.cs | 11 +- .../MaskRCNN/MaskRCNNModelConfiguration.cs | 5 + .../MaskRCNN/MaskRCNNModelOutputParser.cs | 12 +- .../MaskRCNN/MaskRCNNModelRunner.cs | 120 +- .../MaskRCNN/MaskRCNNOnnxModel.cs | 14 +- .../TinyYoloV2/TinyYoloV2OnnxModelRunner.cs | 11 +- Sources/Integrations/Onnx/Common/OnnxModel.cs | 14 +- .../Onnx/Common/OnnxModelRunner.cs | 39 +- .../Onnx/Common/OnnxModelRunner{TIn,TOut}.cs | 87 + .../Onnx/Test.Psi.Onnx/Test.Psi.Onnx.csproj | 1 + .../FFMPEGFrameInfo.cs | 12 +- .../FFMPEGMediaSource.cs | 37 +- .../Microsoft.Psi.Media.Linux/FFMPEGReader.cs | 134 +- .../FFMPEGReaderConfiguration.cs | 4 +- .../Microsoft.Psi.Media.Linux.csproj | 3 - .../Media/Microsoft.Psi.Media.Linux/Readme.md | 19 + .../Microsoft.Psi.Media.Native.x64/Makefile | 21 - .../Microsoft.Psi.Media.Native.x64.cpp | Bin 258 -> 0 bytes .../Microsoft.Psi.Media.Native.x64.vcxproj | 125 - ...osoft.Psi.Media.Native.x64.vcxproj.filters | 45 - .../dllmain.cpp | Bin 912 -> 0 bytes .../Microsoft.Psi.Media.Native.x64/stdafx.cpp | Bin 636 -> 0 bytes .../Microsoft.Psi.Media.Native.x64/stdafx.h | 18 - .../targetver.h | Bin 630 -> 0 bytes .../Microsoft.Psi.Media.Windows.x64.csproj | 7 - .../Mpeg4Writer.cs | 118 +- .../Mpeg4WriterConfiguration.cs | 17 + .../FFMPEGReaderNative.cpp | 16 +- .../FFMPEGReaderNative.h | 7 +- .../Makefile | 30 + .../Readme.md | 6 + .../build.sh | 2 +- .../AssemblyInfo.cpp | 6 +- .../AssemblyInfo.rc | Bin 5104 -> 5048 bytes .../FFMPEGReader.cpp | 127 - .../FFMPEGReader.h | 109 - .../MP4Writer.cpp | 11 +- .../MP4Writer.h | 5 +- ...soft.Psi.Media_Interop.Windows.x64.vcxproj | 46 +- .../HoloLensCapture/HoloLensCapture.sln | 7 - .../HoloLensCaptureApp/HoloLensCaptureApp.cs | 48 +- .../Properties/AssemblyInfo.cs | 6 +- .../HoloLensCaptureExporter/DataExporter.cs | 326 ++- .../HoloLensCaptureExporter/Operators.cs | 128 +- .../HoloLensCaptureExporter/Readme.md | 16 +- .../HoloLensCaptureInterop/Serializers.cs | 326 ++- .../HoloLensCaptureServer.cs | 102 +- .../HoloLensCaptureServer/Readme.md | 2 +- .../MixedReality/HoloLensCapture/Readme.md | 2 +- .../ImageToJpegStreamEncoder.cs | 9 +- .../Microphone.cs} | 30 +- .../MicrophoneConfiguration.cs} | 6 +- .../MixedRealityCapturePerspective.cs | 2 +- .../MixedRealityCaptureVideoEffect.cs | 2 +- .../{ => MediaCapture}/PhotoVideoCamera.cs | 2 +- .../PhotoVideoCameraConfiguration.cs | 2 +- .../{ => MediaCapture}/UnsafeNative.cs | 2 +- ...t.Psi.MixedReality.UniversalWindows.csproj | 43 +- .../MixedReality.cs | 63 +- .../Properties/AssemblyInfo.cs | 6 +- .../{ => ResearchMode}/Accelerometer.cs | 2 +- .../{ => ResearchMode}/DepthCamera.cs | 2 +- .../DepthCameraConfiguration.cs | 2 +- .../{ => ResearchMode}/Gyroscope.cs | 2 +- .../{ => ResearchMode}/Magnetometer.cs | 2 +- .../{ => ResearchMode}/ResearchModeCamera.cs | 2 +- .../ResearchModeCameraConfiguration.cs | 2 +- .../{ => ResearchMode}/ResearchModeImu.cs | 2 +- .../{ => ResearchMode}/VisibleLightCamera.cs | 2 +- .../VisibleLightCameraConfiguration.cs | 2 +- .../SceneUnderstanding.cs | 3 +- .../WinRT/GazeSensor.cs | 118 + .../WinRT/GazeSensorConfiguration.cs | 30 + .../WinRT/Operators.cs | 35 + .../OpenXR/HandVisualizationObject.cs | 107 + .../OpenXR/ProjectHandsToCamerasTask.cs | 140 + .../ProjectHandsToCamerasTask.cs | 152 -- .../HandVisualizationObject.cs | 26 +- .../StereoKit/ProjectHandsToCamerasTask.cs | 141 + .../Microsoft.Psi.MixedReality.csproj | 2 +- .../Microsoft.Psi.MixedReality/OpenXR/Hand.cs | 104 + .../OpenXR/HandsSensor.cs | 222 ++ .../OpenXR/OpenXR.cs | 684 +++++ .../OpenXR/Operators.cs | 65 + .../Microsoft.Psi.MixedReality/Operators.cs | 188 +- .../{ => StereoKit}/EyesSensor.cs | 19 +- .../{ => StereoKit}/Hand.cs | 13 +- .../{ => StereoKit}/Handle.cs | 14 +- .../{ => StereoKit}/HandsSensor.cs | 27 +- .../{ => StereoKit}/HeadSensor.cs | 3 +- .../{ => StereoKit}/Microphone.cs | 20 +- .../MicrophoneConfiguration.cs | 2 +- .../StereoKit/Operators.cs | 157 ++ .../{ => StereoKit}/PsiInput.cs | 11 +- .../Renderers/Box3DRenderer.cs} | 24 +- .../EncodedImageRectangle3DRenderer.cs} | 14 +- .../Renderers/HandsRenderer.cs} | 10 +- .../Renderers/Mesh3DRenderer.cs} | 14 +- .../Renderers/MeshRenderer.cs} | 16 +- .../Renderers/Rectangle3DRenderer.cs} | 16 +- .../Renderers/StereoKitRenderer.cs | 13 +- .../Renderers/TextRenderer.cs} | 22 +- .../Renderers/TextRendererConfiguration.cs} | 8 +- .../{ => StereoKit}/SpatialSound.cs | 58 +- .../{ => StereoKit}/StereoKitComponent.cs | 6 +- .../{ => StereoKit}/StereoKitTransforms.cs | 7 +- .../Microsoft.Psi.MixedReality/TimeHelper.cs | 62 +- .../Microsoft.Psi.MixedReality/WinRT/Eyes.cs | 90 + .../Properties/AssemblyInfo.cs | 6 +- .../AssemblyInfo.cpp | 6 +- .../Rendezvous/Operators.cs | 40 +- .../Rendezvous/Rendezvous.cs | 42 +- .../Runtime/Microsoft.Psi/Common/Envelope.cs | 2 +- ...AdjacentValuesInterpolator{TIn,TResult}.cs | 4 +- .../AdjacentValuesInterpolator{T}.cs | 19 +- .../Microsoft.Psi/Common/KeyedSharedPool.cs | 27 +- .../Runtime/Microsoft.Psi/Common/Metadata.cs | 123 +- .../Microsoft.Psi/Common/PsiStreamMetadata.cs | 46 +- .../Microsoft.Psi/Common/RuntimeInfo.cs | 35 +- .../Runtime/Microsoft.Psi/Common/Shared.cs | 14 +- .../Microsoft.Psi/Common/SharedContainer.cs | 22 +- .../Microsoft.Psi/Common/SharedPool.cs | 60 +- .../Common/TypeResolutionHelper.cs | 14 + .../Microsoft.Psi/Common/UnmanagedArray.cs | 18 +- .../Microsoft.Psi/Common/UnmanagedBuffer.cs | 82 +- .../Microsoft.Psi/Components/Generator.cs | 7 + .../Runtime/Microsoft.Psi/Data/Exporter.cs | 2 +- .../Runtime/Microsoft.Psi/Data/PsiStore.cs | 4 +- .../Data/PsiStoreStreamReader.cs | 11 +- .../Diagnostics/PipelineDiagnostics.cs | 26 + .../Executive/DebugExtensions.cs | 2 +- .../Executive/PipelineElement.cs | 36 +- .../Persistence/InfiniteFileReader.cs | 9 +- .../Persistence/InfiniteFileWriter.cs | 6 +- .../Persistence/MessageWriter.cs | 2 +- .../Persistence/MetadataCache.cs | 18 +- .../Persistence/PsiStoreReader.cs | 26 +- .../Persistence/PsiStoreWriter.cs | 5 +- .../Remoting/RemoteClockImporter.cs | 4 +- .../Microsoft.Psi/Remoting/RemoteExporter.cs | 2 +- .../Scheduling/FutureWorkItemQueue.cs | 14 +- .../Microsoft.Psi/Scheduling/Scheduler.cs | 31 +- .../Serialization/ArraySerializer.cs | 16 +- .../BackCompatClassSerializer.cs | 22 + .../Serialization/BackCompatSerializer.cs | 119 + .../BackCompatStructSerializer.cs | 22 + .../Serialization/BufferSerializer.cs | 16 +- .../Serialization/ByteArraySerializer.cs | 14 +- .../Serialization/ClassSerializer.cs | 14 +- .../Serialization/DictionarySerializer.cs | 14 +- .../DynamicMessageDeserializer.cs | 47 +- .../Serialization/EnumerableSerializer.cs | 14 +- .../Serialization/ISerializer.cs | 2 +- .../Serialization/ImmutableSerializer.cs | 3 +- .../Serialization/KnownSerializers.cs | 114 +- .../Serialization/MemoryStreamSerializer.cs | 14 +- .../Serialization/SimpleArraySerializer.cs | 14 +- .../Serialization/SimpleSerializer.cs | 3 +- .../Serialization/StringArraySerializer.cs | 14 +- .../Serialization/StringSerializer.cs | 4 +- .../Serialization/StructSerializer.cs | 4 +- .../Microsoft.Psi/Serialization/TypeSchema.cs | 116 +- .../Runtime/Test.Psi/SerializationTester.cs | 34 +- Sources/Runtime/Test.Psi/SharedTester.cs | 27 - Sources/Runtime/Test.Psi/Test.Psi.csproj | 2 - ...meraViewAsPointCloudVisualizationObject.cs | 78 + .../LinearVelocity3DVisualizationObject.cs | 6 +- ...intCloud3DDictionaryVisualizationObject.cs | 56 + .../AngularVelocity3D.cs | 145 +- .../Bounds3D.cs | 5 + .../Microsoft.Psi.Spatial.Euclidean/Box3D.cs | 5 + .../CoordinateSystemVelocity3D.cs | 149 +- .../LinearVelocity3D.cs | 116 +- .../Operators.cs | 50 +- .../PointCloud3D.cs | 14 + .../Microsoft.Psi.PsiStudio/MainWindow.xaml | 58 +- .../MainWindow.xaml.cs | 12 +- .../MainWindowViewModel.cs | 274 +- .../Microsoft.Psi.PsiStudio.csproj | 3 +- .../PsiStudioSettings.cs | 220 +- .../PsiStudioSettingsViewModel.cs | 371 +++ .../PsiStudioTemplateSelector.cs | 2 +- .../Windows/PsiStudioSettingsWindow.xaml | 54 + .../Windows/PsiStudioSettingsWindow.xaml.cs | 90 + .../Windows/SettingsWindow.xaml | 66 - .../Windows/SettingsWindow.xaml.cs | 48 - .../Microsoft.Msagl.WpfGraphControl.csproj | 4 +- .../Adapters/ChainedStreamAdapter.cs | 48 + .../Adapters/DictionaryKeyToValueAdapter.cs | 47 + .../Adapters/InterfaceAdapter.cs | 2 +- .../Adapters/ScriptAdapter.cs | 65 + .../Common/ContextMenuName.cs | 19 +- .../Common/IconSourcePath.cs | 15 +- .../ContextMenuItemInfo.cs | 97 + .../Controls/TimelineScroller.cs | 4 +- .../Data/DataManager.cs | 19 +- .../Data/DataStoreReader.cs | 13 +- .../Data/IStreamDataProvider.cs | 5 +- .../StreamAdapter{TSource,TDestination}.cs | 19 + .../Data/StreamBinding.cs | 265 +- .../Data/StreamDataProvider{T}.cs | 3 +- .../Data/StreamIntervalProvider.cs | 4 +- .../Data/StreamValueProvider{TSource}.cs | 4 +- .../DataTypes/ScriptGlobals.cs | 35 + .../Helpers/CollectionHelper.cs | 42 + .../Helpers/DateTimeFormatHelper.cs | 48 - .../Helpers/DateTimeHelper.cs | 127 + .../{SizeFormatHelper.cs => SizeHelper.cs} | 43 +- ...eSpanFormatHelper.cs => TimeSpanHelper.cs} | 2 +- .../TypeSpec.cs | 20 +- .../IContextMenuItemsSource.cs | 21 + .../Icons/set-annotation-to-selection.png | Bin 0 -> 1465 bytes ...Microsoft.Psi.Visualization.Windows.csproj | 25 +- .../Navigation/Navigator.cs | 4 +- .../PluginMap.cs | 58 +- .../PropertyGridEditors/ItemPickerEditor.xaml | 8 + .../ItemPickerEditor.xaml.cs | 32 + .../ItemPickerViewModel.cs | 76 + .../Themes/PropertyGrid.xaml | 4 +- .../TypeKeyedActionCommand.cs | 65 - .../TypeKeyedActionCommand{TKey,TParam}.cs | 35 - .../ViewModels/DatasetViewModel.cs | 44 +- .../ViewModels/DerivedMemberStreamTreeNode.cs | 207 -- ...erivedReceiverDiagnosticsStreamTreeNode.cs | 109 - .../DerivedStreamContainerTreeNode.cs | 70 - .../ViewModels/DerivedStreamTreeNode.cs | 40 - .../ViewModels/PartitionViewModel.cs | 185 +- .../PipelineDiagnosticsStreamTreeNode.cs | 242 -- .../ViewModels/SessionViewModel.cs | 104 +- .../ViewModels/StreamContainerTreeNode.cs | 718 ----- .../ViewModels/StreamTreeNode.cs | 2317 ++++++++++++++--- .../Views/ContextMenuItemsSourceType.cs | 31 - .../Views/IContextMenuItemsSource.cs | 30 - .../InstantVisualizationContainerView.xaml.cs | 72 +- .../Views/InstantVisualizationPanelView.cs | 73 - ...tVisualizationPlaceholderPanelView.xaml.cs | 2 - .../Views/NavigatorView.xaml.cs | 4 - .../TimelineVisualizationPanelView.xaml.cs | 27 - .../Views/VisualizationContainerView.xaml.cs | 175 +- .../Views/VisualizationObjectView.cs | 47 +- .../Views/VisualizationPanelView.cs | 71 +- .../AudioVisualizationObjectView.xaml.cs | 14 - .../DepthImageVisualizationObjectView.xaml.cs | 38 +- .../PipelineDiagnosticsVisualizationModel.cs | 2 +- ...DiagnosticsVisualizationObjectView.xaml.cs | 59 +- ...pelineDiagnosticsVisualizationPresenter.cs | 16 +- .../PlotSeriesVisualizationObjectView.cs | 28 +- .../PlotVisualizationObjectView.cs | 26 - ...lAnnotationVisualizationObjectView.xaml.cs | 79 +- ...alAnnotationVisualizationObjectViewItem.cs | 147 +- .../Views/Visuals3D/AnimatedModelVisual.xaml | 8 - .../Visuals3D/AnimatedModelVisual.xaml.cs | 86 - .../Views/XYVisualizationPanelView.xaml.cs | 17 - .../VisualizationContext.cs | 99 +- .../AnimatedModel3DVisualizationObject.cs | 47 - .../Annotations/AnnotationValueEditor.cs | 1 - .../TimeIntervalAnnotationDisplayData.cs | 6 +- ...meIntervalAnnotationVisualizationObject.cs | 622 +++-- .../AudioVisualizationObject.cs | 26 +- .../CoordinateSystemVisualizationObject.cs | 2 - .../DepthImageVisualizationObject.cs | 27 + ...elVisual3DCollectionVisualizationObject.cs | 1 - .../ModelVisual3DVisualizationObject.cs | 6 +- .../PlotSeriesVisualizationObject.cs | 21 +- .../PlotVisualizationObject{TData}.cs | 23 +- .../PipelineDiagnosticsVisualizationObject.cs | 167 +- .../PosedModelFromFileVisualizationObject.cs | 181 ++ ...treamIntervalVisualizationObject{TData}.cs | 4 +- .../StreamVisualizationObject{TData}.cs | 84 +- .../VisualizationContainer.cs | 63 +- .../VisualizationObject.cs | 21 +- .../XYValueVisualizationObject.cs | 1 - .../InstantVisualizationContainer.cs | 25 +- .../InstantVisualizationPanel.cs | 68 +- .../TimelineVisualizationPanel.cs | 201 +- .../VisualizationPanels/VisualizationPanel.cs | 234 +- .../XYVisualizationPanel.cs | 293 +-- .../VisualizerMetadata.cs | 110 +- .../Windows/ConfirmLayoutWindow.xaml | 30 + .../Windows/ConfirmLayoutWindow.xaml.cs | 34 + .../CreateAnnotationStreamWindow.xaml.cs | 14 +- .../Windows/ExportPsiPartitionWindow.xaml.cs | 4 +- .../Windows/ProgressWindow.xaml | 6 + .../Windows/ProgressWindow.xaml.cs | 9 +- .../Windows/RunBatchProcessingTaskWindow.xaml | 51 +- .../RunBatchProcessingTaskWindow.xaml.cs | 28 +- .../RunBatchProcessingTaskWindowViewModel.cs | 289 +- .../Windows/ScriptWindow.xaml | 79 + .../Windows/ScriptWindow.xaml.cs | 360 +++ .../Properties/AssemblyInfo.cs | 6 +- build.sh | 2 +- 317 files changed, 12161 insertions(+), 6876 deletions(-) rename Sources/{Visualization/Microsoft.Psi.Visualization.Windows => Data/Microsoft.Psi.Data}/Helpers/IndexHelper.cs (88%) rename Sources/{Visualization/Microsoft.Psi.Visualization.Windows/Common/NearestMessageType.cs => Data/Microsoft.Psi.Data/Helpers/NearestType.cs} (86%) delete mode 100644 Sources/Imaging/Microsoft.Psi.Imaging.Windows/ImageToGZipStreamEncoder.cs rename Sources/{MixedReality/Microsoft.Psi.MixedReality.UniversalWindows => Imaging/Microsoft.Psi.Imaging}/ImageToGzipStreamEncoder.cs (100%) delete mode 100644 Sources/Imaging/Test.Psi.Imaging.Windows/Properties/AssemblyInfo.cs create mode 100644 Sources/Integrations/Onnx/Common/OnnxModelRunner{TIn,TOut}.cs rename Sources/Media/{Shared => Microsoft.Psi.Media.Linux}/FFMPEGMediaSource.cs (91%) create mode 100644 Sources/Media/Microsoft.Psi.Media.Linux/Readme.md delete mode 100644 Sources/Media/Microsoft.Psi.Media.Native.x64/Makefile delete mode 100644 Sources/Media/Microsoft.Psi.Media.Native.x64/Microsoft.Psi.Media.Native.x64.cpp delete mode 100644 Sources/Media/Microsoft.Psi.Media.Native.x64/Microsoft.Psi.Media.Native.x64.vcxproj delete mode 100644 Sources/Media/Microsoft.Psi.Media.Native.x64/Microsoft.Psi.Media.Native.x64.vcxproj.filters delete mode 100644 Sources/Media/Microsoft.Psi.Media.Native.x64/dllmain.cpp delete mode 100644 Sources/Media/Microsoft.Psi.Media.Native.x64/stdafx.cpp delete mode 100644 Sources/Media/Microsoft.Psi.Media.Native.x64/stdafx.h delete mode 100644 Sources/Media/Microsoft.Psi.Media.Native.x64/targetver.h rename Sources/Media/{Microsoft.Psi.Media.Native.x64 => Microsoft.Psi.Media_Interop.Linux}/FFMPEGReaderNative.cpp (98%) rename Sources/Media/{Microsoft.Psi.Media.Native.x64 => Microsoft.Psi.Media_Interop.Linux}/FFMPEGReaderNative.h (98%) create mode 100644 Sources/Media/Microsoft.Psi.Media_Interop.Linux/Makefile create mode 100644 Sources/Media/Microsoft.Psi.Media_Interop.Linux/Readme.md rename Sources/Media/{Microsoft.Psi.Media.Native.x64 => Microsoft.Psi.Media_Interop.Linux}/build.sh (91%) mode change 100755 => 100644 delete mode 100644 Sources/Media/Microsoft.Psi.Media_Interop.Windows.x64/FFMPEGReader.cpp delete mode 100644 Sources/Media/Microsoft.Psi.Media_Interop.Windows.x64/FFMPEGReader.h rename Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/{MediaCaptureMicrophone.cs => MediaCapture/Microphone.cs} (95%) rename Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/{MediaCaptureMicrophoneConfiguration.cs => MediaCapture/MicrophoneConfiguration.cs} (86%) rename Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/{ => MediaCapture}/MixedRealityCapturePerspective.cs (93%) rename Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/{ => MediaCapture}/MixedRealityCaptureVideoEffect.cs (98%) rename Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/{ => MediaCapture}/PhotoVideoCamera.cs (99%) rename Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/{ => MediaCapture}/PhotoVideoCameraConfiguration.cs (99%) rename Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/{ => MediaCapture}/UnsafeNative.cs (95%) rename Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/{ => ResearchMode}/Accelerometer.cs (95%) rename Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/{ => ResearchMode}/DepthCamera.cs (99%) rename Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/{ => ResearchMode}/DepthCameraConfiguration.cs (98%) rename Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/{ => ResearchMode}/Gyroscope.cs (95%) rename Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/{ => ResearchMode}/Magnetometer.cs (95%) rename Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/{ => ResearchMode}/ResearchModeCamera.cs (99%) rename Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/{ => ResearchMode}/ResearchModeCameraConfiguration.cs (98%) rename Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/{ => ResearchMode}/ResearchModeImu.cs (99%) rename Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/{ => ResearchMode}/VisibleLightCamera.cs (98%) rename Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/{ => ResearchMode}/VisibleLightCameraConfiguration.cs (97%) create mode 100644 Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/WinRT/GazeSensor.cs create mode 100644 Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/WinRT/GazeSensorConfiguration.cs create mode 100644 Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/WinRT/Operators.cs create mode 100644 Sources/MixedReality/Microsoft.Psi.MixedReality.Visualization.Windows/OpenXR/HandVisualizationObject.cs create mode 100644 Sources/MixedReality/Microsoft.Psi.MixedReality.Visualization.Windows/OpenXR/ProjectHandsToCamerasTask.cs delete mode 100644 Sources/MixedReality/Microsoft.Psi.MixedReality.Visualization.Windows/ProjectHandsToCamerasTask.cs rename Sources/MixedReality/Microsoft.Psi.MixedReality.Visualization.Windows/{ => StereoKit}/HandVisualizationObject.cs (84%) create mode 100644 Sources/MixedReality/Microsoft.Psi.MixedReality.Visualization.Windows/StereoKit/ProjectHandsToCamerasTask.cs create mode 100644 Sources/MixedReality/Microsoft.Psi.MixedReality/OpenXR/Hand.cs create mode 100644 Sources/MixedReality/Microsoft.Psi.MixedReality/OpenXR/HandsSensor.cs create mode 100644 Sources/MixedReality/Microsoft.Psi.MixedReality/OpenXR/OpenXR.cs create mode 100644 Sources/MixedReality/Microsoft.Psi.MixedReality/OpenXR/Operators.cs rename Sources/MixedReality/Microsoft.Psi.MixedReality/{ => StereoKit}/EyesSensor.cs (80%) rename Sources/MixedReality/Microsoft.Psi.MixedReality/{ => StereoKit}/Hand.cs (90%) rename Sources/MixedReality/Microsoft.Psi.MixedReality/{ => StereoKit}/Handle.cs (90%) rename Sources/MixedReality/Microsoft.Psi.MixedReality/{ => StereoKit}/HandsSensor.cs (74%) rename Sources/MixedReality/Microsoft.Psi.MixedReality/{ => StereoKit}/HeadSensor.cs (97%) rename Sources/MixedReality/Microsoft.Psi.MixedReality/{ => StereoKit}/Microphone.cs (81%) rename Sources/MixedReality/Microsoft.Psi.MixedReality/{ => StereoKit}/MicrophoneConfiguration.cs (94%) create mode 100644 Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/Operators.cs rename Sources/MixedReality/Microsoft.Psi.MixedReality/{ => StereoKit}/PsiInput.cs (93%) rename Sources/MixedReality/Microsoft.Psi.MixedReality/{Renderers/Box3DStereoKitRenderer.cs => StereoKit/Renderers/Box3DRenderer.cs} (73%) rename Sources/MixedReality/Microsoft.Psi.MixedReality/{Renderers/EncodedImageRectangle3DStereoKitRenderer.cs => StereoKit/Renderers/EncodedImageRectangle3DRenderer.cs} (77%) rename Sources/MixedReality/Microsoft.Psi.MixedReality/{Renderers/HandsStereoKitRenderer.cs => StereoKit/Renderers/HandsRenderer.cs} (87%) rename Sources/MixedReality/Microsoft.Psi.MixedReality/{Renderers/Mesh3DStereoKitRenderer.cs => StereoKit/Renderers/Mesh3DRenderer.cs} (73%) rename Sources/MixedReality/Microsoft.Psi.MixedReality/{Renderers/MeshStereoKitRenderer.cs => StereoKit/Renderers/MeshRenderer.cs} (89%) rename Sources/MixedReality/Microsoft.Psi.MixedReality/{Renderers/Rectangle3DStereoKitRenderer.cs => StereoKit/Renderers/Rectangle3DRenderer.cs} (76%) rename Sources/MixedReality/Microsoft.Psi.MixedReality/{ => StereoKit}/Renderers/StereoKitRenderer.cs (77%) rename Sources/MixedReality/Microsoft.Psi.MixedReality/{Renderers/TextStereoKitRenderer.cs => StereoKit/Renderers/TextRenderer.cs} (91%) rename Sources/MixedReality/Microsoft.Psi.MixedReality/{Renderers/TextStereoKitRendererConfiguration.cs => StereoKit/Renderers/TextRendererConfiguration.cs} (94%) rename Sources/MixedReality/Microsoft.Psi.MixedReality/{ => StereoKit}/SpatialSound.cs (78%) rename Sources/MixedReality/Microsoft.Psi.MixedReality/{ => StereoKit}/StereoKitComponent.cs (94%) rename Sources/MixedReality/Microsoft.Psi.MixedReality/{ => StereoKit}/StereoKitTransforms.cs (83%) create mode 100644 Sources/MixedReality/Microsoft.Psi.MixedReality/WinRT/Eyes.cs create mode 100644 Sources/Runtime/Microsoft.Psi/Serialization/BackCompatClassSerializer.cs create mode 100644 Sources/Runtime/Microsoft.Psi/Serialization/BackCompatSerializer.cs create mode 100644 Sources/Runtime/Microsoft.Psi/Serialization/BackCompatStructSerializer.cs create mode 100644 Sources/Spatial/Microsoft.Psi.Spatial.Euclidean.Visualization.Windows/PointCloud3DDictionaryVisualizationObject.cs create mode 100644 Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/PsiStudioSettingsViewModel.cs create mode 100644 Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/Windows/PsiStudioSettingsWindow.xaml create mode 100644 Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/Windows/PsiStudioSettingsWindow.xaml.cs delete mode 100644 Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/Windows/SettingsWindow.xaml delete mode 100644 Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/Windows/SettingsWindow.xaml.cs create mode 100644 Sources/Visualization/Microsoft.Psi.Visualization.Windows/Adapters/ChainedStreamAdapter.cs create mode 100644 Sources/Visualization/Microsoft.Psi.Visualization.Windows/Adapters/DictionaryKeyToValueAdapter.cs create mode 100644 Sources/Visualization/Microsoft.Psi.Visualization.Windows/Adapters/ScriptAdapter.cs create mode 100644 Sources/Visualization/Microsoft.Psi.Visualization.Windows/ContextMenuItemInfo.cs create mode 100644 Sources/Visualization/Microsoft.Psi.Visualization.Windows/DataTypes/ScriptGlobals.cs create mode 100644 Sources/Visualization/Microsoft.Psi.Visualization.Windows/Helpers/CollectionHelper.cs delete mode 100644 Sources/Visualization/Microsoft.Psi.Visualization.Windows/Helpers/DateTimeFormatHelper.cs create mode 100644 Sources/Visualization/Microsoft.Psi.Visualization.Windows/Helpers/DateTimeHelper.cs rename Sources/Visualization/Microsoft.Psi.Visualization.Windows/Helpers/{SizeFormatHelper.cs => SizeHelper.cs} (68%) rename Sources/Visualization/Microsoft.Psi.Visualization.Windows/Helpers/{TimeSpanFormatHelper.cs => TimeSpanHelper.cs} (97%) rename Sources/Visualization/Microsoft.Psi.Visualization.Windows/{Views/Visuals2D/DiagnosticsVisualization => Helpers}/TypeSpec.cs (93%) create mode 100644 Sources/Visualization/Microsoft.Psi.Visualization.Windows/IContextMenuItemsSource.cs create mode 100644 Sources/Visualization/Microsoft.Psi.Visualization.Windows/Icons/set-annotation-to-selection.png create mode 100644 Sources/Visualization/Microsoft.Psi.Visualization.Windows/PropertyGridEditors/ItemPickerEditor.xaml create mode 100644 Sources/Visualization/Microsoft.Psi.Visualization.Windows/PropertyGridEditors/ItemPickerEditor.xaml.cs create mode 100644 Sources/Visualization/Microsoft.Psi.Visualization.Windows/PropertyGridEditors/ItemPickerViewModel.cs delete mode 100644 Sources/Visualization/Microsoft.Psi.Visualization.Windows/TypeKeyedActionCommand.cs delete mode 100644 Sources/Visualization/Microsoft.Psi.Visualization.Windows/TypeKeyedActionCommand{TKey,TParam}.cs delete mode 100644 Sources/Visualization/Microsoft.Psi.Visualization.Windows/ViewModels/DerivedMemberStreamTreeNode.cs delete mode 100644 Sources/Visualization/Microsoft.Psi.Visualization.Windows/ViewModels/DerivedReceiverDiagnosticsStreamTreeNode.cs delete mode 100644 Sources/Visualization/Microsoft.Psi.Visualization.Windows/ViewModels/DerivedStreamContainerTreeNode.cs delete mode 100644 Sources/Visualization/Microsoft.Psi.Visualization.Windows/ViewModels/DerivedStreamTreeNode.cs delete mode 100644 Sources/Visualization/Microsoft.Psi.Visualization.Windows/ViewModels/PipelineDiagnosticsStreamTreeNode.cs delete mode 100644 Sources/Visualization/Microsoft.Psi.Visualization.Windows/ViewModels/StreamContainerTreeNode.cs delete mode 100644 Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/ContextMenuItemsSourceType.cs delete mode 100644 Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/IContextMenuItemsSource.cs delete mode 100644 Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals3D/AnimatedModelVisual.xaml delete mode 100644 Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals3D/AnimatedModelVisual.xaml.cs delete mode 100644 Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/AnimatedModel3DVisualizationObject.cs create mode 100644 Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/PosedModelFromFileVisualizationObject.cs create mode 100644 Sources/Visualization/Microsoft.Psi.Visualization.Windows/Windows/ConfirmLayoutWindow.xaml create mode 100644 Sources/Visualization/Microsoft.Psi.Visualization.Windows/Windows/ConfirmLayoutWindow.xaml.cs create mode 100644 Sources/Visualization/Microsoft.Psi.Visualization.Windows/Windows/ScriptWindow.xaml create mode 100644 Sources/Visualization/Microsoft.Psi.Visualization.Windows/Windows/ScriptWindow.xaml.cs diff --git a/Directory.Build.props b/Directory.Build.props index 04ffdd660..05760788f 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -6,7 +6,7 @@ Microsoft Corporation microsoft,psi Microsoft - 0.17.52.1 + 0.18.72.1 $(AssemblyVersion) $(AssemblyVersion)-beta false diff --git a/Psi.sln b/Psi.sln index 55b8270ae..0229f718f 100644 --- a/Psi.sln +++ b/Psi.sln @@ -152,8 +152,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{4026C2BE Build\Test.Psi.ruleset = Build\Test.Psi.ruleset EndProjectSection EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Microsoft.Psi.Media.Native.x64", "Sources\Media\Microsoft.Psi.Media.Native.x64\Microsoft.Psi.Media.Native.x64.vcxproj", "{C50F7F21-BEB0-4366-B73F-859EEBC3ED42}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RealSense", "RealSense", "{64BBFFEA-7CFB-49F9-AC74-3AD1D13245FC}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Psi.RealSense.Windows.x64", "Sources\RealSense\Microsoft.Psi.RealSense.Windows.x64\Microsoft.Psi.RealSense.Windows.x64.csproj", "{7B73D864-9997-4637-8765-44C17FD09CE1}" @@ -393,10 +391,6 @@ Global {4478A162-4FE9-4737-A630-3899DC5935C6}.Debug|Any CPU.Build.0 = Debug|Any CPU {4478A162-4FE9-4737-A630-3899DC5935C6}.Release|Any CPU.ActiveCfg = Release|Any CPU {4478A162-4FE9-4737-A630-3899DC5935C6}.Release|Any CPU.Build.0 = Release|Any CPU - {C50F7F21-BEB0-4366-B73F-859EEBC3ED42}.Debug|Any CPU.ActiveCfg = Debug|x64 - {C50F7F21-BEB0-4366-B73F-859EEBC3ED42}.Debug|Any CPU.Build.0 = Debug|x64 - {C50F7F21-BEB0-4366-B73F-859EEBC3ED42}.Release|Any CPU.ActiveCfg = Release|x64 - {C50F7F21-BEB0-4366-B73F-859EEBC3ED42}.Release|Any CPU.Build.0 = Release|x64 {7B73D864-9997-4637-8765-44C17FD09CE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7B73D864-9997-4637-8765-44C17FD09CE1}.Debug|Any CPU.Build.0 = Debug|Any CPU {7B73D864-9997-4637-8765-44C17FD09CE1}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -582,7 +576,6 @@ Global {B9F00634-88A1-40EF-9DAD-814A307AD81F} = {C6976E06-9D12-4398-8096-C23D04E63F61} {BE194924-7162-405D-BF6E-E6086BAA12F1} = {3F77CC04-2E58-452B-8107-0C93E7944D4E} {4478A162-4FE9-4737-A630-3899DC5935C6} = {CB8286F5-167B-4416-8FE9-9B97FCF146D5} - {C50F7F21-BEB0-4366-B73F-859EEBC3ED42} = {AD8FE445-240A-4791-8C64-A5E2D6E67FF9} {64BBFFEA-7CFB-49F9-AC74-3AD1D13245FC} = {A0856299-D28A-4513-B964-3FA5290FF160} {7B73D864-9997-4637-8765-44C17FD09CE1} = {64BBFFEA-7CFB-49F9-AC74-3AD1D13245FC} {DAB8847B-DE0A-45E2-A7DA-30432A36525B} = {64BBFFEA-7CFB-49F9-AC74-3AD1D13245FC} diff --git a/Sources/Calibration/Microsoft.Psi.Calibration/CalibrationExtensions.cs b/Sources/Calibration/Microsoft.Psi.Calibration/CalibrationExtensions.cs index 654240236..469fee074 100644 --- a/Sources/Calibration/Microsoft.Psi.Calibration/CalibrationExtensions.cs +++ b/Sources/Calibration/Microsoft.Psi.Calibration/CalibrationExtensions.cs @@ -7,6 +7,7 @@ namespace Microsoft.Psi.Calibration using System.Collections.Generic; using MathNet.Numerics.LinearAlgebra; using MathNet.Spatial.Euclidean; + using MathNet.Spatial.Units; using Microsoft.Psi; using Microsoft.Psi.Imaging; @@ -196,8 +197,8 @@ public static ICameraIntrinsics CreateCameraIntrinsics(this ImageBase image, dou double z = pointInCameraSpace.X * colorExtrinsicsInverse[2, 0] + pointInCameraSpace.Y * colorExtrinsicsInverse[2, 1] + pointInCameraSpace.Z * colorExtrinsicsInverse[2, 2] + colorExtrinsicsInverse[2, 3]; var pointInDepthCameraSpace = new Point3D(x, y, z); var colorCameraOriginInDepthCameraSpace = new Point3D(colorExtrinsicsInverse[0, 3], colorExtrinsicsInverse[1, 3], colorExtrinsicsInverse[2, 3]); - var searchLine = new Line3D(colorCameraOriginInDepthCameraSpace, pointInDepthCameraSpace); - return IntersectLineWithDepthMesh(depthDeviceCalibrationInfo.DepthIntrinsics, searchLine, depthImage.Resource); + var searchRay = new Ray3D(colorCameraOriginInDepthCameraSpace, pointInDepthCameraSpace - colorCameraOriginInDepthCameraSpace); + return depthImage.Resource.ComputeRayIntersection(depthDeviceCalibrationInfo.DepthIntrinsics, searchRay); } /// @@ -213,41 +214,6 @@ public static ICameraIntrinsics CreateCameraIntrinsics(this ImageBase image, dou string name = nameof(ProjectTo3D)) => source.PipeTo(new ProjectTo3D(source.Out.Pipeline, name), deliveryPolicy); - /// - /// Performs a ray/mesh intersection with the depth map. - /// - /// The intrinsics for the depth camera. - /// Ray to intersect against depth map. - /// Depth map to ray cast against. - /// The maximum distance to search for. - /// Distance to march on each step along ray. - /// Whether undistortion should be applied to the point. - /// Returns point of intersection. - public static Point3D? IntersectLineWithDepthMesh(ICameraIntrinsics depthIntrinsics, Line3D line, DepthImage depthImage, double maxDistance = 5, double skipFactor = 0.05, bool undistort = true) - { - // max distance to check for intersection with the scene - var delta = skipFactor * (line.EndPoint - line.StartPoint).Normalize(); - - // size of increment along the ray - int maxSteps = (int)(maxDistance / delta.Length); - var hypothesisPoint = line.StartPoint; - for (int i = 0; i < maxSteps; i++) - { - hypothesisPoint += delta; - - // get the mesh distance at the extended point - float meshDistance = GetMeshDepthAtPoint(depthIntrinsics, depthImage, hypothesisPoint, undistort); - - // if the mesh distance is less than the distance to the point we've hit the mesh - if (!float.IsNaN(meshDistance) && (meshDistance < hypothesisPoint.X)) - { - return hypothesisPoint; - } - } - - return null; - } - /// /// Use the Rodrigues formula for transforming a given rotation from axis-angle representation to a 3x3 matrix. /// Where 'r' is a rotation vector: @@ -299,13 +265,15 @@ public static Matrix AxisAngleToMatrix(Vector vectorRotation) } /// - /// Convert a rotation matrix to axis-angle representation (a unit vector scaled by the angular distance to rotate). + /// Convert a rotation matrix to axis-angle representation (a unit vector scaled by the angular distance in radians to rotate). /// /// Input rotation matrix. - /// Same rotation in axis-angle representation (L2-Norm of the vector represents angular distance). /// An optional angle epsilon parameter used to determine when the specified matrix contains a zero-rotation (by default 0.01 degrees). - public static Vector MatrixToAxisAngle(Matrix m, double epsilon = 0.01 * Math.PI / 180) + /// Same rotation in axis-angle representation (L2-Norm of the vector represents angular distance in radians). + public static Vector MatrixToAxisAngle(Matrix m, Angle? epsilon = null) { + epsilon ??= Angle.FromDegrees(0.01); + if (m.RowCount != 3 || m.ColumnCount != 3) { throw new InvalidOperationException("The input must be a valid 3x3 rotation matrix in order to compute its axis-angle representation."); @@ -317,7 +285,7 @@ public static Vector MatrixToAxisAngle(Matrix m, double epsilon // Create the axis vector. var v = Vector.Build.Dense(3, 0); - if (double.IsNaN(angle) || angle < epsilon) + if (double.IsNaN(angle) || angle < epsilon.Value.Radians) { // If the angular distance to rotate is 0, we just return a vector of all zeroes. return v; @@ -427,28 +395,62 @@ public static void Project(Matrix cameraMatrix, Vector distCoeff projectedPoint = new Point2D(fx * xpp + cx, fy * ypp + cy); } - private static float GetMeshDepthAtPoint(ICameraIntrinsics depthIntrinsics, DepthImage depthImage, Point3D point, bool undistort) + /// + /// Computes a ray intersection with a depth image mesh. + /// + /// Depth image mesh to ray cast against. + /// The intrinsics for the depth camera. + /// Ray to intersect against depth image mesh. + /// The maximum distance to search for (default is 5 meters). + /// Distance to march on each step along ray (default is 5 cm). + /// Whether undistortion should be applied to the point. + /// Returns point of intersection, or null if no intersection was found. + /// + /// The ray is assumed to be defined relative to the pose of the depth camera, + /// i.e., (0, 0, 0) is the position of the camera itself. + /// + public static Point3D? ComputeRayIntersection(this DepthImage depthImage, ICameraIntrinsics depthIntrinsics, Ray3D ray, double maxDistance = 5, double skipFactor = 0.05, bool undistort = true) { - if (!depthIntrinsics.TryGetPixelPosition(point, undistort, out var depthPixel)) - { - return float.NaN; - } + // max distance to check for intersection with the scene + int maxSteps = (int)(maxDistance / skipFactor); - int x = (int)Math.Round(depthPixel.X); - int y = (int)Math.Round(depthPixel.Y); - if ((x < 0) || (x >= depthImage.Width) || (y < 0) || (y >= depthImage.Height)) - { - return float.NaN; - } + // size of increment along the ray + var delta = skipFactor * ray.Direction; - int byteOffset = (int)((y * depthImage.Stride) + (x * 2)); - var depth = BitConverter.ToUInt16(depthImage.ReadBytes(2, byteOffset), 0); - if (depth == 0) + var hypothesisPoint = ray.ThroughPoint; + for (int i = 0; i < maxSteps; i++) { - return float.NaN; + hypothesisPoint += delta; + + // get the mesh distance at the hypothesis point + if (depthIntrinsics.TryGetPixelPosition(hypothesisPoint, undistort, out var depthPixel) && + depthImage.TryGetPixel((int)Math.Floor(depthPixel.X), (int)Math.Floor(depthPixel.Y), out var depthValue) && + depthValue != 0) + { + // if the mesh distance is less than the distance to the point we've hit the mesh + var meshDistanceMeters = (double)depthValue * depthImage.DepthValueToMetersScaleFactor; + if (depthImage.DepthValueSemantics == DepthValueSemantics.DistanceToPlane) + { + if (meshDistanceMeters < hypothesisPoint.X) + { + return hypothesisPoint; + } + } + else if (depthImage.DepthValueSemantics == DepthValueSemantics.DistanceToPoint) + { + if (meshDistanceMeters < hypothesisPoint.ToVector3D().Length) + { + return hypothesisPoint; + } + } + else + { + throw new ArgumentException($"Unhandled {nameof(DepthValueSemantics)}: {depthImage.DepthValueSemantics}"); + } + } } - return (float)depth / 1000; + return null; } private static double CalibrateCamera( diff --git a/Sources/Calibration/Microsoft.Psi.Calibration/CameraIntrinsics.cs b/Sources/Calibration/Microsoft.Psi.Calibration/CameraIntrinsics.cs index 56d035089..57c6ee8cd 100644 --- a/Sources/Calibration/Microsoft.Psi.Calibration/CameraIntrinsics.cs +++ b/Sources/Calibration/Microsoft.Psi.Calibration/CameraIntrinsics.cs @@ -141,6 +141,13 @@ private set public Point2D? GetPixelPosition(Point3D point3D, bool distort, bool nullIfOutsideFieldOfView = true) { // X points in the depth dimension. Y points to the left, and Z points up. + + // If the point is not in front of the camera, we cannot compute the projection + if (point3D.X <= 0) + { + return null; + } + var point2D = new Point2D(-point3D.Y / point3D.X, -point3D.Z / point3D.X); if (distort) { diff --git a/Sources/Calibration/Microsoft.Psi.Calibration/ICameraIntrinsics.cs b/Sources/Calibration/Microsoft.Psi.Calibration/ICameraIntrinsics.cs index 660d47fca..85b38d6af 100644 --- a/Sources/Calibration/Microsoft.Psi.Calibration/ICameraIntrinsics.cs +++ b/Sources/Calibration/Microsoft.Psi.Calibration/ICameraIntrinsics.cs @@ -77,6 +77,8 @@ public interface ICameraIntrinsics : IEquatable /// Indicates whether to apply distortion. /// Optional flag indicating whether to return null if point is outside the field of view (default true). /// Point containing the pixel position. + /// Points that are behind the camera, i.e., with the X value below zero lead to null returns, + /// regardless of value of the parameter. Point2D? GetPixelPosition(Point3D point3D, bool distort, bool nullIfOutsideFieldOfView = true); /// @@ -87,6 +89,8 @@ public interface ICameraIntrinsics : IEquatable /// Output point containing the pixel position. /// Optional flag indicating whether to return null if point is outside the field of view (default true). /// True if is within field of view, otherwise false. + /// Points that are behind the camera, i.e., with the X value below zero lead to a return value of false, + /// regardless of value of the parameter. bool TryGetPixelPosition(Point3D point3D, bool distort, out Point2D pixelPosition, bool nullIfOutsideFieldOfView = true); /// diff --git a/Sources/Data/Microsoft.Psi.Data/Annotations/Operators.cs b/Sources/Data/Microsoft.Psi.Data/Annotations/Operators.cs index 087fc3078..8a22c544e 100644 --- a/Sources/Data/Microsoft.Psi.Data/Annotations/Operators.cs +++ b/Sources/Data/Microsoft.Psi.Data/Annotations/Operators.cs @@ -103,6 +103,103 @@ public static class Operators deliveryPolicy, name); + /// + /// Determines whether any annotation on a specified track intersects with the specified time interval. + /// + /// The set of annotations. + /// The track name. + /// The time interval. + /// An output parameter returning the time interval for the intersecting annotation. + /// True if any annotation on the specified track intersects with the specified time interval, otherwise false. + public static bool GetAnnotationTimeIntervalOverlappingWith( + this IEnumerable annotations, + string track, + TimeInterval timeInterval, + out TimeInterval intersectingTimeInterval) + { + // set the intersectingTimeInterval to empty + intersectingTimeInterval = null; + + // Start by building up a list of indices where the annotation set has an annotation on + // the specified track. (We will need to search among the annotations for these indices) + var annotationsOnTrack = annotations.GetTimeIntervalAnnotations(track); + + // If there's no annotations at all, we're done and there's no intersection. + if (annotationsOnTrack.Count == 0) + { + return false; + } + + // Find the nearest annotation to the left edge of the interval + int index = IndexHelper.GetIndexForTime(timeInterval.Left, annotationsOnTrack.Count, i => annotationsOnTrack[i].Interval.Right, NearestType.Nearest); + + // Check if the annotation intersects with the interval, then keep walking to the right until + // we find an annotation within the interval or we go past the right hand side of the interval. + while (index < annotationsOnTrack.Count) + { + var annotation = annotationsOnTrack[index]; + + // Check if the annotation intersects with the interval + // NOTE: By default time intervals are inclusive of their endpoints, so abutting time intervals will + // test as intersecting. Use a non-inclusive time interval so that we can let annotations abut. + if (timeInterval.IntersectsWith(new TimeInterval(annotation.Interval.Left, false, annotation.Interval.Right, false))) + { + intersectingTimeInterval = annotation.Interval; + return true; + } + + // Check if the annotation is completely to the right of the interval + if (timeInterval.Right <= annotation.Interval.Left) + { + return false; + } + + index++; + } + + return false; + } + + /// + /// Gets the time interval annotation at a specified time on a specified track, or null if none exists. + /// + /// The collection of annotations. + /// The time. + /// The track name. + /// The time interval annotation at a specified time on a specified track, or null if none exists. + public static TimeIntervalAnnotation GetTimeIntervalAnnotationAtTime(this IEnumerable annotations, DateTime time, string track) + { + if (track == null) + { + return null; + } + + var annotationsOnTrack = annotations.GetTimeIntervalAnnotations(track); + + if (annotationsOnTrack.Count > 0) + { + var index = GetTimeIntervalItemIndexByTime( + time, + annotationsOnTrack.Count, + i => annotationsOnTrack[i].Interval.Left, + i => annotationsOnTrack[i].Interval.Right); + return index == -1 ? null : annotationsOnTrack[index]; + } + else + { + return null; + } + } + + /// + /// Gets the list of time interval annotations on the specified track. + /// + /// The collection of annotations. + /// The track name. + /// The list of time interval annotations on the specified track. + public static List GetTimeIntervalAnnotations(this IEnumerable annotations, string track) + => annotations.Where(tias => tias.ContainsTrack(track)).Select(tias => tias[track]).ToList(); + /// /// Converts a stream into a corresponding stream of time interval annotations. /// @@ -219,5 +316,49 @@ public static class Operators return source.PipeTo(processor, deliveryPolicy); } + + private static int GetTimeIntervalItemIndexByTime(DateTime time, int count, Func startTimeAtIndex, Func endTimeAtIndex) + { + if (count < 1) + { + return -1; + } + + int lo = 0; + int hi = count - 1; + while ((lo != hi - 1) && (lo != hi)) + { + var val = (lo + hi) / 2; + if (endTimeAtIndex(val) < time) + { + lo = val; + } + else if (startTimeAtIndex(val) > time) + { + hi = val; + } + else + { + return val; + } + } + + // If lo and hi differ by 1, then either of those value could be straddled by the first or last + // annotation. If lo and hi are both 0 then there's only 1 element so we should test it as well. + if (hi - lo <= 1) + { + if ((endTimeAtIndex(hi) >= time) && (startTimeAtIndex(hi) <= time)) + { + return hi; + } + + if ((endTimeAtIndex(lo) >= time) && (startTimeAtIndex(lo) <= time)) + { + return lo; + } + } + + return -1; + } } } diff --git a/Sources/Data/Microsoft.Psi.Data/BatchProcessingTaskConfiguration.cs b/Sources/Data/Microsoft.Psi.Data/BatchProcessingTaskConfiguration.cs index 25030aed1..f413bce76 100644 --- a/Sources/Data/Microsoft.Psi.Data/BatchProcessingTaskConfiguration.cs +++ b/Sources/Data/Microsoft.Psi.Data/BatchProcessingTaskConfiguration.cs @@ -4,7 +4,10 @@ namespace Microsoft.Psi.Data { using System.ComponentModel; + using System.IO; using System.Runtime.Serialization; + using Microsoft.Psi.Data.Helpers; + using Newtonsoft.Json; /// /// Represents a configuration for a batch processing task. @@ -103,6 +106,53 @@ public string OutputPartitionName set { this.Set(nameof(this.OutputPartitionName), ref this.outputPartitionName, value); } } + /// + /// Loads a configuration from the specified file. + /// + /// The full path name of the configuration file. + /// The loaded configuration. + public static BatchProcessingTaskConfiguration Load(string fileName) + { + var serializer = JsonSerializer.Create( + new JsonSerializerSettings() + { + Formatting = Formatting.Indented, + NullValueHandling = NullValueHandling.Ignore, + ReferenceLoopHandling = ReferenceLoopHandling.Ignore, + TypeNameHandling = TypeNameHandling.Auto, + TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple, + SerializationBinder = new SafeSerializationBinder(), + PreserveReferencesHandling = PreserveReferencesHandling.Objects, + }); + + using var jsonFile = File.OpenText(fileName); + using var jsonReader = new JsonTextReader(jsonFile); + return serializer.Deserialize(jsonReader); + } + + /// + /// Saves the configuration to a file. + /// + /// The full path name of the configuration file. + public void Save(string fileName) + { + var serializer = JsonSerializer.Create( + new JsonSerializerSettings() + { + Formatting = Formatting.Indented, + NullValueHandling = NullValueHandling.Ignore, + ReferenceLoopHandling = ReferenceLoopHandling.Ignore, + TypeNameHandling = TypeNameHandling.Auto, + TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple, + SerializationBinder = new SafeSerializationBinder(), + PreserveReferencesHandling = PreserveReferencesHandling.Objects, + }); + + using var jsonFile = File.CreateText(fileName); + using var jsonWriter = new JsonTextWriter(jsonFile); + serializer.Serialize(jsonWriter, this, typeof(BatchProcessingTaskConfiguration)); + } + /// /// Validates the configuration. /// diff --git a/Sources/Data/Microsoft.Psi.Data/BatchProcessingTaskMetadata.cs b/Sources/Data/Microsoft.Psi.Data/BatchProcessingTaskMetadata.cs index 3cf0b2cfd..88c661b3c 100644 --- a/Sources/Data/Microsoft.Psi.Data/BatchProcessingTaskMetadata.cs +++ b/Sources/Data/Microsoft.Psi.Data/BatchProcessingTaskMetadata.cs @@ -4,6 +4,7 @@ namespace Microsoft.Psi.Data { using System; + using System.IO; using System.Reflection; /// @@ -15,16 +16,19 @@ public class BatchProcessingTaskMetadata private readonly BatchProcessingTaskAttribute batchProcessingTaskAttribute = null; private readonly Type batchProcessingTaskType; private readonly MethodInfo batchProcessingTaskMethodInfo; + private readonly string batchProcessingTaskConfigurationsPath; /// /// Initializes a new instance of the class. /// /// The batch processing task type. /// The batch processing task attribute. - public BatchProcessingTaskMetadata(Type batchProcessingTaskType, BatchProcessingTaskAttribute batchProcessingTaskAttribute) + /// The folder in which batch processing task configurations are saved. + public BatchProcessingTaskMetadata(Type batchProcessingTaskType, BatchProcessingTaskAttribute batchProcessingTaskAttribute, string batchProcessingTaskConfigurationsPath) { this.batchProcessingTaskType = batchProcessingTaskType; this.batchProcessingTaskAttribute = batchProcessingTaskAttribute; + this.batchProcessingTaskConfigurationsPath = Path.Combine(batchProcessingTaskConfigurationsPath, batchProcessingTaskAttribute.Name); } /// @@ -32,10 +36,12 @@ public BatchProcessingTaskMetadata(Type batchProcessingTaskType, BatchProcessing /// /// The batch processing method info. /// The batch processing task attribute. - public BatchProcessingTaskMetadata(MethodInfo batchProcessingTaskMethodInfo, BatchProcessingTaskAttribute batchProcessingTaskAttribute) + /// The folder in which batch processing task configurations are saved. + public BatchProcessingTaskMetadata(MethodInfo batchProcessingTaskMethodInfo, BatchProcessingTaskAttribute batchProcessingTaskAttribute, string batchProcessingTaskConfigurationsPath) { this.batchProcessingTaskMethodInfo = batchProcessingTaskMethodInfo; this.batchProcessingTaskAttribute = batchProcessingTaskAttribute; + this.batchProcessingTaskConfigurationsPath = Path.Combine(batchProcessingTaskConfigurationsPath, batchProcessingTaskAttribute.Name); } /// @@ -53,6 +59,16 @@ public BatchProcessingTaskMetadata(MethodInfo batchProcessingTaskMethodInfo, Bat /// public string IconSourcePath => this.batchProcessingTaskAttribute.IconSourcePath; + /// + /// Gets the folder under which configurations for this batch processing task should be stored. + /// + public string ConfigurationsPath => this.batchProcessingTaskConfigurationsPath; + + /// + /// Gets or sets the name of the most recently used configuration. + /// + public string MostRecentlyUsedConfiguration { get; set; } + /// /// Gets a value indicating whether this batch processing task is method based. /// diff --git a/Sources/Data/Microsoft.Psi.Data/Dataset.cs b/Sources/Data/Microsoft.Psi.Data/Dataset.cs index c23d8b961..b63e192d4 100644 --- a/Sources/Data/Microsoft.Psi.Data/Dataset.cs +++ b/Sources/Data/Microsoft.Psi.Data/Dataset.cs @@ -85,11 +85,31 @@ public string Name /// Gets the originating time interval (earliest to latest) of the messages in this dataset. /// [IgnoreDataMember] - public TimeInterval OriginatingTimeInterval => + public TimeInterval MessageOriginatingTimeInterval => TimeInterval.Coverage( this.InternalSessions - .Where(s => s.OriginatingTimeInterval.Left > DateTime.MinValue && s.OriginatingTimeInterval.Right < DateTime.MaxValue) - .Select(s => s.OriginatingTimeInterval)); + .Where(s => s.MessageOriginatingTimeInterval.Left > DateTime.MinValue && s.MessageOriginatingTimeInterval.Right < DateTime.MaxValue) + .Select(s => s.MessageOriginatingTimeInterval)); + + /// + /// Gets the creation time interval (earliest to latest) of the messages in this dataset. + /// + [IgnoreDataMember] + public TimeInterval MessageCreationTimeInterval => + TimeInterval.Coverage( + this.InternalSessions + .Where(s => s.MessageCreationTimeInterval.Left > DateTime.MinValue && s.MessageCreationTimeInterval.Right < DateTime.MaxValue) + .Select(s => s.MessageCreationTimeInterval)); + + /// + /// Gets the stream open-close time interval in this dataset. + /// + [IgnoreDataMember] + public TimeInterval TimeInterval => + TimeInterval.Coverage( + this.InternalSessions + .Where(s => s.TimeInterval.Left > DateTime.MinValue && s.TimeInterval.Right < DateTime.MaxValue) + .Select(s => s.TimeInterval)); /// /// Gets the size of the dataset, in bytes. @@ -450,10 +470,10 @@ public Session AddSessionFromStore(IStreamReader streamReader, string sessionNam var sessionStart = this.Sessions.Select(s => { var currentDuration = totalDuration; - totalDuration += s.OriginatingTimeInterval.Span.TotalSeconds; + totalDuration += s.TimeInterval.Span.TotalSeconds; return currentDuration; }).ToList(); - var sessionDuration = this.Sessions.Select(s => s.OriginatingTimeInterval.Span.TotalSeconds).ToList(); + var sessionDuration = this.Sessions.Select(s => s.TimeInterval.Span.TotalSeconds).ToList(); for (int i = 0; i < this.Sessions.Count; i++) { diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Helpers/IndexHelper.cs b/Sources/Data/Microsoft.Psi.Data/Helpers/IndexHelper.cs similarity index 88% rename from Sources/Visualization/Microsoft.Psi.Visualization.Windows/Helpers/IndexHelper.cs rename to Sources/Data/Microsoft.Psi.Data/Helpers/IndexHelper.cs index b7ba74f7c..6663b2fbb 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Helpers/IndexHelper.cs +++ b/Sources/Data/Microsoft.Psi.Data/Helpers/IndexHelper.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -namespace Microsoft.Psi.Visualization.Helpers +namespace Microsoft.Psi.Data { using System; @@ -11,15 +11,14 @@ namespace Microsoft.Psi.Visualization.Helpers public static class IndexHelper { /// - /// Gets the index id corresponding to a specified time for - /// a collection of indices that are ordered by time. + /// Gets the index corresponding to a specified time for a collection of indices that are ordered by time. /// /// The time to find the index for. - /// The number of items in the index collection. - /// A function that returns the time for a given index id. - /// Timeline snapping behaviors. + /// The number of items in the collection. + /// A function that returns the time for a given index. + /// Specifies whether to look for nearest, next, or previous index. /// The index id closest to the specified time, using the specified interpolation style. - public static int GetIndexForTime(DateTime dateTime, int count, Func timeAtIndex, NearestMessageType snappingBehavior = NearestMessageType.Nearest) + public static int GetIndexForTime(DateTime dateTime, int count, Func timeAtIndex, NearestType nearestType = NearestType.Nearest) { // If there's only one point in the index, then return its index if (count == 1) @@ -37,10 +36,10 @@ public static int GetIndexForTime(DateTime dateTime, int count, Func result.LowIndex, - NearestMessageType.Next => result.HighIndex, + NearestType.Previous => result.LowIndex, + NearestType.Next => result.HighIndex, // o/w return the index of whichever point is closest to the current time _ => (timeAtIndex(result.HighIndex) - dateTime) < (dateTime - timeAtIndex(result.LowIndex)) ? result.HighIndex : result.LowIndex, diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Common/NearestMessageType.cs b/Sources/Data/Microsoft.Psi.Data/Helpers/NearestType.cs similarity index 86% rename from Sources/Visualization/Microsoft.Psi.Visualization.Windows/Common/NearestMessageType.cs rename to Sources/Data/Microsoft.Psi.Data/Helpers/NearestType.cs index 1f078ca4a..c67ad2ecd 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Common/NearestMessageType.cs +++ b/Sources/Data/Microsoft.Psi.Data/Helpers/NearestType.cs @@ -1,12 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -namespace Microsoft.Psi.Visualization.Helpers +namespace Microsoft.Psi.Data { /// /// Defines various modes for finding a nearest message to a specified time. /// - public enum NearestMessageType + public enum NearestType { /// /// The nearest message. diff --git a/Sources/Data/Microsoft.Psi.Data/IPartition.cs b/Sources/Data/Microsoft.Psi.Data/IPartition.cs index 5bed2ce0e..51903586f 100644 --- a/Sources/Data/Microsoft.Psi.Data/IPartition.cs +++ b/Sources/Data/Microsoft.Psi.Data/IPartition.cs @@ -23,7 +23,17 @@ public interface IPartition /// /// Gets the originating time interval (earliest to latest) of the messages in this partition. /// - TimeInterval OriginatingTimeInterval { get; } + TimeInterval MessageOriginatingTimeInterval { get; } + + /// + /// Gets the creation time interval (earliest to latest) of the messages in this partition. + /// + TimeInterval MessageCreationTimeInterval { get; } + + /// + /// Gets the time interval between open and closed time for all streams in this partition. + /// + TimeInterval TimeInterval { get; } /// /// Gets the size of the partition in bytes, if known. diff --git a/Sources/Data/Microsoft.Psi.Data/Partition{TStreamReader}.cs b/Sources/Data/Microsoft.Psi.Data/Partition{TStreamReader}.cs index e6905b0ef..d5aca57e3 100644 --- a/Sources/Data/Microsoft.Psi.Data/Partition{TStreamReader}.cs +++ b/Sources/Data/Microsoft.Psi.Data/Partition{TStreamReader}.cs @@ -85,7 +85,15 @@ public string Name /// [IgnoreDataMember] - public TimeInterval OriginatingTimeInterval { get; private set; } = TimeInterval.Empty; + public TimeInterval MessageOriginatingTimeInterval { get; private set; } = TimeInterval.Empty; + + /// + [IgnoreDataMember] + public TimeInterval MessageCreationTimeInterval { get; private set; } = TimeInterval.Empty; + + /// + [IgnoreDataMember] + public TimeInterval TimeInterval { get; private set; } = TimeInterval.Empty; /// [IgnoreDataMember] @@ -108,7 +116,9 @@ private set if (this.streamReader != null) { // Set originating time interval from the reader metadata - this.OriginatingTimeInterval = this.streamReader.MessageOriginatingTimeInterval; + this.MessageOriginatingTimeInterval = this.streamReader.MessageOriginatingTimeInterval; + this.MessageCreationTimeInterval = this.streamReader.MessageCreationTimeInterval; + this.TimeInterval = this.streamReader.StreamTimeInterval; this.Size = this.streamReader.Size; this.StreamCount = this.streamReader.StreamCount; } diff --git a/Sources/Data/Microsoft.Psi.Data/Session.cs b/Sources/Data/Microsoft.Psi.Data/Session.cs index 115dbe7ad..099a105c6 100644 --- a/Sources/Data/Microsoft.Psi.Data/Session.cs +++ b/Sources/Data/Microsoft.Psi.Data/Session.cs @@ -77,11 +77,37 @@ public string Name /// Gets the originating time interval (earliest to latest) of the messages in this session. /// [IgnoreDataMember] - public TimeInterval OriginatingTimeInterval => + public TimeInterval MessageOriginatingTimeInterval => TimeInterval.Coverage( this.InternalPartitions - .Where(p => p.OriginatingTimeInterval.Left > DateTime.MinValue && p.OriginatingTimeInterval.Right < DateTime.MaxValue) - .Select(p => p.OriginatingTimeInterval)); + .Where(p => p.MessageOriginatingTimeInterval.Left > DateTime.MinValue && p.MessageOriginatingTimeInterval.Right < DateTime.MaxValue) + .Select(p => p.MessageOriginatingTimeInterval)); + + /// + /// Gets the creation time interval (earliest to latest) of the messages in this session. + /// + [IgnoreDataMember] + public TimeInterval MessageCreationTimeInterval => + TimeInterval.Coverage( + this.InternalPartitions + .Where(p => p.MessageCreationTimeInterval.Left > DateTime.MinValue && p.MessageCreationTimeInterval.Right < DateTime.MaxValue) + .Select(p => p.MessageCreationTimeInterval)); + + /// + /// Gets the stream open-close time interval in this session. + /// + [IgnoreDataMember] + public TimeInterval TimeInterval => + TimeInterval.Coverage( + this.InternalPartitions + .Where(p => p.TimeInterval.Left > DateTime.MinValue && p.TimeInterval.Right < DateTime.MaxValue) + .Select(p => p.TimeInterval)); + + /// + /// Gets the session duration. + /// + [IgnoreDataMember] + public TimeSpan Duration => this.TimeInterval.Span; /// /// Gets the size of the session, in bytes. @@ -102,6 +128,13 @@ public string Name [DataMember(Name = "Partitions")] private List InternalPartitions { get; set; } + /// + /// Gets the partition specified by a name. + /// + /// The name of the partition. + /// The partition with the specified name. + public IPartition this[string partitionName] => this.InternalPartitions.FirstOrDefault(p => p.Name == partitionName); + /// /// Creates and adds a data partition from an existing data store. /// diff --git a/Sources/Data/Test.Psi.Data/DatasetTests.cs b/Sources/Data/Test.Psi.Data/DatasetTests.cs index 104daedda..6e23e0b8a 100644 --- a/Sources/Data/Test.Psi.Data/DatasetTests.cs +++ b/Sources/Data/Test.Psi.Data/DatasetTests.cs @@ -37,7 +37,7 @@ public void DatasetAddSession() { var dataset = new Dataset(); Assert.AreEqual(0, dataset.Sessions.Count); - Assert.IsTrue(dataset.OriginatingTimeInterval.IsEmpty); + Assert.IsTrue(dataset.MessageOriginatingTimeInterval.IsEmpty); // generate a test store GenerateTestStore("PsiStore", StorePath); @@ -48,8 +48,8 @@ public void DatasetAddSession() Assert.AreEqual("Session_0", dataset.Sessions[0].Name); // verify originating time interval - Assert.AreEqual(session0.OriginatingTimeInterval.Left, dataset.OriginatingTimeInterval.Left); - Assert.AreEqual(session0.OriginatingTimeInterval.Right, dataset.OriginatingTimeInterval.Right); + Assert.AreEqual(session0.MessageOriginatingTimeInterval.Left, dataset.MessageOriginatingTimeInterval.Left); + Assert.AreEqual(session0.MessageOriginatingTimeInterval.Right, dataset.MessageOriginatingTimeInterval.Right); // generate a new store with a different originating time interval than the first GenerateTestStore("NewStore", StorePath); @@ -61,8 +61,8 @@ public void DatasetAddSession() Assert.AreEqual("Session_1", dataset.Sessions[1].Name); // verify new originating time interval - Assert.AreEqual(session0.OriginatingTimeInterval.Left, dataset.OriginatingTimeInterval.Left); - Assert.AreEqual(session1.OriginatingTimeInterval.Right, dataset.OriginatingTimeInterval.Right); + Assert.AreEqual(session0.MessageOriginatingTimeInterval.Left, dataset.MessageOriginatingTimeInterval.Left); + Assert.AreEqual(session1.MessageOriginatingTimeInterval.Right, dataset.MessageOriginatingTimeInterval.Right); } [TestMethod] @@ -104,8 +104,8 @@ public void DatasetAppend() Assert.AreEqual("Session_0", dataset.Sessions[0].Name); // verify originating time interval - Assert.AreEqual(session0.OriginatingTimeInterval.Left, dataset.OriginatingTimeInterval.Left); - Assert.AreEqual(session0.OriginatingTimeInterval.Right, dataset.OriginatingTimeInterval.Right); + Assert.AreEqual(session0.MessageOriginatingTimeInterval.Left, dataset.MessageOriginatingTimeInterval.Left); + Assert.AreEqual(session0.MessageOriginatingTimeInterval.Right, dataset.MessageOriginatingTimeInterval.Right); // generate a new store with a different originating time interval than the first GenerateTestStore("NewStore", StorePath); @@ -120,8 +120,8 @@ public void DatasetAppend() Assert.AreEqual("Session_1", dataset.Sessions[1].Name); // verify new originating time interval - Assert.AreEqual(session0.OriginatingTimeInterval.Left, dataset.OriginatingTimeInterval.Left); - Assert.AreEqual(session1.OriginatingTimeInterval.Right, dataset.OriginatingTimeInterval.Right); + Assert.AreEqual(session0.MessageOriginatingTimeInterval.Left, dataset.MessageOriginatingTimeInterval.Left); + Assert.AreEqual(session1.MessageOriginatingTimeInterval.Right, dataset.MessageOriginatingTimeInterval.Right); } [TestMethod] @@ -167,10 +167,10 @@ public void SessionAddPartition() Assert.AreEqual("Partition_0", session.Partitions[0].Name); // verify new originating time intervals (session and dataset) - Assert.AreEqual(partition0.OriginatingTimeInterval.Left, session.OriginatingTimeInterval.Left); - Assert.AreEqual(partition0.OriginatingTimeInterval.Right, session.OriginatingTimeInterval.Right); - Assert.AreEqual(partition0.OriginatingTimeInterval.Left, dataset.OriginatingTimeInterval.Left); - Assert.AreEqual(partition0.OriginatingTimeInterval.Right, dataset.OriginatingTimeInterval.Right); + Assert.AreEqual(partition0.MessageOriginatingTimeInterval.Left, session.MessageOriginatingTimeInterval.Left); + Assert.AreEqual(partition0.MessageOriginatingTimeInterval.Right, session.MessageOriginatingTimeInterval.Right); + Assert.AreEqual(partition0.MessageOriginatingTimeInterval.Left, dataset.MessageOriginatingTimeInterval.Left); + Assert.AreEqual(partition0.MessageOriginatingTimeInterval.Right, dataset.MessageOriginatingTimeInterval.Right); // generate a new store with a different originating time interval than the first GenerateTestStore("NewStore", StorePath); @@ -182,10 +182,10 @@ public void SessionAddPartition() Assert.AreEqual("Partition_1", session.Partitions[1].Name); // verify new originating time intervals (session and dataset) - Assert.AreEqual(partition0.OriginatingTimeInterval.Left, session.OriginatingTimeInterval.Left); - Assert.AreEqual(partition1.OriginatingTimeInterval.Right, session.OriginatingTimeInterval.Right); - Assert.AreEqual(partition0.OriginatingTimeInterval.Left, dataset.OriginatingTimeInterval.Left); - Assert.AreEqual(partition1.OriginatingTimeInterval.Right, dataset.OriginatingTimeInterval.Right); + Assert.AreEqual(partition0.MessageOriginatingTimeInterval.Left, session.MessageOriginatingTimeInterval.Left); + Assert.AreEqual(partition1.MessageOriginatingTimeInterval.Right, session.MessageOriginatingTimeInterval.Right); + Assert.AreEqual(partition0.MessageOriginatingTimeInterval.Left, dataset.MessageOriginatingTimeInterval.Left); + Assert.AreEqual(partition1.MessageOriginatingTimeInterval.Right, dataset.MessageOriginatingTimeInterval.Right); } [TestMethod] @@ -420,8 +420,8 @@ public void DatasetAutoSave() Assert.AreEqual(1, sameDataset.Sessions.Count); Assert.AreEqual(session2.Name, sameDataset.Sessions[0].Name); Assert.AreEqual(1, sameDataset.Sessions[0].Partitions.Count); - Assert.AreEqual(session2.OriginatingTimeInterval.Left, sameDataset.Sessions[0].OriginatingTimeInterval.Left); - Assert.AreEqual(session2.OriginatingTimeInterval.Right, sameDataset.Sessions[0].OriginatingTimeInterval.Right); + Assert.AreEqual(session2.MessageOriginatingTimeInterval.Left, sameDataset.Sessions[0].MessageOriginatingTimeInterval.Left); + Assert.AreEqual(session2.MessageOriginatingTimeInterval.Right, sameDataset.Sessions[0].MessageOriginatingTimeInterval.Right); // now we edit the session and we want to make sure the changes stick! GenerateTestStore("store3", StorePath); diff --git a/Sources/Imaging/Microsoft.Psi.Imaging.Windows/ImageToGZipStreamEncoder.cs b/Sources/Imaging/Microsoft.Psi.Imaging.Windows/ImageToGZipStreamEncoder.cs deleted file mode 100644 index ca19a9f9f..000000000 --- a/Sources/Imaging/Microsoft.Psi.Imaging.Windows/ImageToGZipStreamEncoder.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. - -namespace Microsoft.Psi.Imaging -{ - using System.IO; - using System.IO.Compression; - - /// - /// Implements an image encoder for GZip format. - /// - public class ImageToGZipStreamEncoder : IImageToStreamEncoder - { - /// - public string Description => "GZip"; - - /// - public void EncodeToStream(Image image, Stream stream) - { - unsafe - { - var size = image.Stride * image.Height; - var imageData = new UnmanagedMemoryStream((byte*)image.ImageData.ToPointer(), size); - using var compressor = new GZipStream(stream, CompressionMode.Compress, true); - imageData.CopyTo(compressor); - } - } - } -} \ No newline at end of file diff --git a/Sources/Imaging/Microsoft.Psi.Imaging/DepthImage.cs b/Sources/Imaging/Microsoft.Psi.Imaging/DepthImage.cs index 8f5d5f42d..087daa6e4 100644 --- a/Sources/Imaging/Microsoft.Psi.Imaging/DepthImage.cs +++ b/Sources/Imaging/Microsoft.Psi.Imaging/DepthImage.cs @@ -164,11 +164,6 @@ public void CopyFrom(BitmapData bitmapData) { CheckPixelFormat(bitmapData.PixelFormat); int numBytes = bitmapData.Height * bitmapData.Stride; - if (numBytes > this.UnmanagedBuffer.Size) - { - throw new InvalidOperationException("Buffer too small."); - } - this.UnmanagedBuffer.CopyFrom(bitmapData.Scan0, numBytes); } @@ -187,11 +182,6 @@ public void CopyFrom(Bitmap bitmap) try { int numBytes = bitmapData.Height * bitmapData.Stride; - if (numBytes > this.UnmanagedBuffer.Size) - { - throw new InvalidOperationException("Buffer too small."); - } - this.UnmanagedBuffer.CopyFrom(bitmapData.Scan0, numBytes); } finally @@ -434,8 +424,16 @@ public override TypeSchema Initialize(KnownSerializers serializers, TypeSchema t schemaMembers.Add(new TypeMemberSchema(nameof(DepthImage.depthValueToMetersScaleFactor), typeof(double).AssemblyQualifiedName, false)); var type = typeof(DepthImage); - var name = TypeSchema.GetContractName(type, serializers.RuntimeVersion); - this.Schema = new TypeSchema(name, TypeSchema.GetId(name), type.AssemblyQualifiedName, TypeFlags.IsClass, schemaMembers, Version); + var name = TypeSchema.GetContractName(type, serializers.RuntimeInfo.SerializationSystemVersion); + this.Schema = new TypeSchema( + type.AssemblyQualifiedName, + TypeFlags.IsClass, + schemaMembers, + name, + TypeSchema.GetId(name), + LatestSchemaVersion, + this.GetType().AssemblyQualifiedName, + serializers.RuntimeInfo.SerializationSystemVersion); } else { diff --git a/Sources/Imaging/Microsoft.Psi.Imaging/Image.cs b/Sources/Imaging/Microsoft.Psi.Imaging/Image.cs index ed15fde1d..7e4701cb5 100644 --- a/Sources/Imaging/Microsoft.Psi.Imaging/Image.cs +++ b/Sources/Imaging/Microsoft.Psi.Imaging/Image.cs @@ -94,6 +94,15 @@ public Image(BitmapData bitmapData, bool makeCopy = false) { } + /// + /// Initializes a new instance of the class. + /// + /// The size of the unmanaged buffer that holds the image. + internal Image(int unmanagedBufferSize) + : base(unmanagedBufferSize) + { + } + /// /// Creates a new from a specified bitmap. /// @@ -173,11 +182,6 @@ public void Save(string filename) public void CopyFrom(BitmapData bitmapData) { int numBytes = bitmapData.Height * bitmapData.Stride; - if (numBytes > this.UnmanagedBuffer.Size) - { - throw new InvalidOperationException("Buffer too small."); - } - this.UnmanagedBuffer.CopyFrom(bitmapData.Scan0, numBytes); } @@ -212,11 +216,6 @@ public void CopyFrom(Bitmap bitmap) else { int numBytes = bitmapData.Height * bitmapData.Stride; - if (numBytes > this.UnmanagedBuffer.Size) - { - throw new InvalidOperationException("Buffer too small."); - } - this.UnmanagedBuffer.CopyFrom(bitmapData.Scan0, numBytes); } } diff --git a/Sources/Imaging/Microsoft.Psi.Imaging/ImageBase.cs b/Sources/Imaging/Microsoft.Psi.Imaging/ImageBase.cs index eeb246de4..a3016b8b0 100644 --- a/Sources/Imaging/Microsoft.Psi.Imaging/ImageBase.cs +++ b/Sources/Imaging/Microsoft.Psi.Imaging/ImageBase.cs @@ -124,6 +124,19 @@ public ImageBase(BitmapData bitmapData, bool makeCopy = false) this.pixelFormat = PixelFormatHelper.FromSystemPixelFormat(bitmapData.PixelFormat); } + /// + /// Initializes a new instance of the class. + /// + /// The size of the unmanaged buffer that holds the image. + internal ImageBase(int unmanagedBufferSize) + { + this.image = UnmanagedBuffer.Allocate(unmanagedBufferSize); + this.width = 0; + this.height = 0; + this.stride = 0; + this.pixelFormat = PixelFormat.Undefined; + } + /// /// Gets a pointer to unmanaged buffer that wraps the image data in unmanaged memory. /// @@ -176,7 +189,7 @@ public void Dispose() /// The buffer must be allocated and must have the same size. public void CopyTo(byte[] destinationBuffer) { - this.image.CopyTo(destinationBuffer); + this.image.CopyTo(destinationBuffer, destinationBuffer.Length); } /// @@ -387,7 +400,7 @@ public void CopyFrom(byte[] sourceBuffer, int offset, int length) /// The image must be allocated and must have the same size. public void CopyFrom(IntPtr source) { - this.image.CopyFrom(source, this.image.Size); + this.image.CopyFrom(source, this.Size); } /// @@ -488,6 +501,20 @@ public byte[] ReadBytes(int count, int offset = 0) /// An empty image of the same size. public abstract ImageBase CreateEmptyOfSameSize(); + /// + /// Initialize an image that has been constructed from just a buffer. + /// + /// The width of the image. + /// The height of the image. + /// The pixel format for the image. + internal void Initialize(int width, int height, PixelFormat pixelFormat) + { + this.width = width; + this.height = height; + this.stride = 4 * ((width * pixelFormat.GetBytesPerPixel() + 3) / 4); + this.pixelFormat = pixelFormat; + } + private void CopyImageSlow(IntPtr sourceIntPtr, PixelFormat sourceFormat, IntPtr destinationIntPtr, int destinationStride, PixelFormat destinationFormat) { unsafe @@ -650,7 +677,7 @@ public abstract class CustomSerializer : ISerializer /// /// Gets the schema version for custom image serialization. /// - protected const int Version = 5; + protected const int LatestSchemaVersion = 5; /// public bool? IsClearRequired => true; @@ -680,8 +707,16 @@ public virtual TypeSchema Initialize(KnownSerializers serializers, TypeSchema ta new TypeMemberSchema("pixelFormat", typeof(PixelFormat).AssemblyQualifiedName, true), }; var type = typeof(TImage); - var name = TypeSchema.GetContractName(type, serializers.RuntimeVersion); - this.Schema = new TypeSchema(name, TypeSchema.GetId(name), type.AssemblyQualifiedName, TypeFlags.IsClass, schemaMembers, Version); + var name = TypeSchema.GetContractName(type, serializers.RuntimeInfo.SerializationSystemVersion); + this.Schema = new TypeSchema( + type.AssemblyQualifiedName, + TypeFlags.IsClass, + schemaMembers, + name, + TypeSchema.GetId(name), + LatestSchemaVersion, + this.GetType().AssemblyQualifiedName, + serializers.RuntimeInfo.SerializationSystemVersion); } else { @@ -749,10 +784,7 @@ public virtual void Clone(TImage instance, ref TImage target, SerializationConte /// Serialization context. public virtual void PrepareDeserializationTarget(BufferReader reader, ref TImage target, SerializationContext context) { - if (target == null) - { - target = (TImage)System.Runtime.Serialization.FormatterServices.GetUninitializedObject(typeof(TImage)); - } + target ??= (TImage)System.Runtime.Serialization.FormatterServices.GetUninitializedObject(typeof(TImage)); } /// diff --git a/Sources/Imaging/Microsoft.Psi.Imaging/ImagePool.cs b/Sources/Imaging/Microsoft.Psi.Imaging/ImagePool.cs index 181659fc0..7e24fdc76 100644 --- a/Sources/Imaging/Microsoft.Psi.Imaging/ImagePool.cs +++ b/Sources/Imaging/Microsoft.Psi.Imaging/ImagePool.cs @@ -11,8 +11,23 @@ namespace Microsoft.Psi.Imaging /// public static class ImagePool { - private static readonly KeyedSharedPool Instance = - new KeyedSharedPool(key => new Image(key.width, key.height, key.format)); + private static readonly KeyedSharedPool Instance = new (allocationSize => new Image(allocationSize)); + private static int imageAllocationBlockSize = 1; + + /// + /// Resets the pool of shared images. + /// + /// The image allocation block size to use. + /// Indicates whether to clear any live objects. + /// + /// If the clearLiveObjects flag is false, an exception is thrown if a reset is attempted while the pool + /// still contains live objects. + /// + public static void Reset(int imageAllocationBlockSize, bool clearLiveObjects = false) + { + Instance.Reset(clearLiveObjects); + ImagePool.imageAllocationBlockSize = imageAllocationBlockSize; + } /// /// Gets or creates an image from the pool. @@ -23,7 +38,11 @@ public static class ImagePool /// A shared image from the pool. public static Shared GetOrCreate(int width, int height, PixelFormat pixelFormat) { - return Instance.GetOrCreate((width, height, pixelFormat)); + var size = height * 4 * ((width * pixelFormat.GetBytesPerPixel() + 3) / 4); + var allocationSize = ((size + imageAllocationBlockSize - 1) / imageAllocationBlockSize) * imageAllocationBlockSize; + var sharedImage = Instance.GetOrCreate(allocationSize); + sharedImage.Resource.Initialize(width, height, pixelFormat); + return sharedImage; } /// diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/ImageToGzipStreamEncoder.cs b/Sources/Imaging/Microsoft.Psi.Imaging/ImageToGzipStreamEncoder.cs similarity index 100% rename from Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/ImageToGzipStreamEncoder.cs rename to Sources/Imaging/Microsoft.Psi.Imaging/ImageToGzipStreamEncoder.cs diff --git a/Sources/Imaging/Microsoft.Psi.Imaging/Microsoft.Psi.Imaging.csproj b/Sources/Imaging/Microsoft.Psi.Imaging/Microsoft.Psi.Imaging.csproj index ca1933750..03d8e1111 100644 --- a/Sources/Imaging/Microsoft.Psi.Imaging/Microsoft.Psi.Imaging.csproj +++ b/Sources/Imaging/Microsoft.Psi.Imaging/Microsoft.Psi.Imaging.csproj @@ -37,7 +37,7 @@ - + diff --git a/Sources/Imaging/Test.Psi.Imaging.Windows/ImageTester.cs b/Sources/Imaging/Test.Psi.Imaging.Windows/ImageTester.cs index e15ae8801..6cc8eca8e 100644 --- a/Sources/Imaging/Test.Psi.Imaging.Windows/ImageTester.cs +++ b/Sources/Imaging/Test.Psi.Imaging.Windows/ImageTester.cs @@ -16,49 +16,49 @@ namespace Test.Psi.Imaging [TestClass] public class ImageTester { - private Image testImage_Gray = Image.FromBitmap(Properties.Resources.TestImage_Gray); - private Image testImage_GrayDrawCircle = Image.FromBitmap(Properties.Resources.TestImage_GrayDrawCircle); - private Image testImage_GrayDrawLine = Image.FromBitmap(Properties.Resources.TestImage_GrayDrawLine); - private Image testImage_GrayDrawRect = Image.FromBitmap(Properties.Resources.TestImage_GrayDrawRect); - private Image testImage_GrayDrawText = Image.FromBitmap(Properties.Resources.TestImage_GrayDrawText); - private Image testImage_GrayFillRect = Image.FromBitmap(Properties.Resources.TestImage_GrayFillRect); - private Image testImage_GrayFillCircle = Image.FromBitmap(Properties.Resources.TestImage_GrayFillCircle); - private Image testImage_GrayDrawTextWithBackground = Image.FromBitmap(Properties.Resources.TestImage_GrayDrawTextWithBackground); - private Image testImage_GrayFlip = Image.FromBitmap(Properties.Resources.TestImage_GrayFlip); - private Image testImage_GrayResized = Image.FromBitmap(Properties.Resources.TestImage_GrayResized); - private Image testImage_GrayRotate = Image.FromBitmap(Properties.Resources.TestImage_GrayRotate); - private Image testImage_GraySetPixel = Image.FromBitmap(Properties.Resources.TestImage_GraySetPixel); - private Image testImage_SetPixel = Image.FromBitmap(Properties.Resources.TestImage_SetPixel); - private Image testImage = Image.FromBitmap(Properties.Resources.TestImage); - private Image testImage2 = Image.FromBitmap(Properties.Resources.TestImage2); - private Image testImage2_Threshold = Image.FromBitmap(Properties.Resources.TestImage2_Threshold); - private Image testImage2_RedChannel = Image.FromBitmap(Properties.Resources.TestImage2_RedChannel); - private Image testImage2_GreenChannel = Image.FromBitmap(Properties.Resources.TestImage2_GreenChannel); - private Image testImage2_BlueChannel = Image.FromBitmap(Properties.Resources.TestImage2_BlueChannel); - private Image testImage2_CopyImage = Image.FromBitmap(Properties.Resources.TestImage2_CopyImage); - private Image testImage2_Invert = Image.FromBitmap(Properties.Resources.TestImage2_Invert); - private Image testImage2_Mask = Image.FromBitmap(Properties.Resources.TestImage2_Mask); - private Image testImage2_FlipHoriz = Image.FromBitmap(Properties.Resources.TestImage2_FlipHoriz); - private Image testImage2_FlipVert = Image.FromBitmap(Properties.Resources.TestImage2_FlipVert); - private Image testImage2_Rotate_Neg10 = Image.FromBitmap(Properties.Resources.TestImage2_Rotate_Neg10); - private Image testImage2_Rotate_Neg10_Loose = Image.FromBitmap(Properties.Resources.TestImage2_Rotate_Neg10_Loose); - private Image testImage2_Rotate_110 = Image.FromBitmap(Properties.Resources.TestImage2_Rotate_110); - private Image testImage2_Rotate_110_Loose = Image.FromBitmap(Properties.Resources.TestImage2_Rotate_110_Loose); - private Image testImage2_DrawRect = Image.FromBitmap(Properties.Resources.TestImage2_DrawRect); - private Image testImage2_DrawLine = Image.FromBitmap(Properties.Resources.TestImage2_DrawLine); - private Image testImage2_DrawCircle = Image.FromBitmap(Properties.Resources.TestImage2_DrawCircle); - private Image testImage2_DrawText = Image.FromBitmap(Properties.Resources.TestImage2_DrawText); - private Image testImage2_FillRect = Image.FromBitmap(Properties.Resources.TestImage2_FillRect); - private Image testImage2_FillCircle = Image.FromBitmap(Properties.Resources.TestImage2_FillCircle); - private Image testImage2_DrawTextWithBackground = Image.FromBitmap(Properties.Resources.TestImage2_DrawTextWithBackground); - private Image testImage2_AbsDiff = Image.FromBitmap(Properties.Resources.TestImage2_AbsDiff); - private Image testImage_0_0_200_100 = Image.FromBitmap(Properties.Resources.TestImage_Crop_0_0_200_100); - private Image testImage_153_57_103_199 = Image.FromBitmap(Properties.Resources.TestImage_Crop_153_57_103_199); - private Image testImage_73_41_59_37 = Image.FromBitmap(Properties.Resources.TestImage_Crop_73_41_59_37); - private Image testImage_50_25_Cubic = Image.FromBitmap(Properties.Resources.TestImage_Scale_50_25_Cubic); - private Image testImage_150_125_Point = Image.FromBitmap(Properties.Resources.TestImage_Scale_150_125_Point); - private Image testImage_25_200_Linear = Image.FromBitmap(Properties.Resources.TestImage_Scale_25_200_Linear); - private Image solidColorsImage = Image.FromBitmap(Properties.Resources.SolidColors); + private readonly Image testImage_Gray = Image.FromBitmap(Properties.Resources.TestImage_Gray); + private readonly Image testImage_GrayDrawCircle = Image.FromBitmap(Properties.Resources.TestImage_GrayDrawCircle); + private readonly Image testImage_GrayDrawLine = Image.FromBitmap(Properties.Resources.TestImage_GrayDrawLine); + private readonly Image testImage_GrayDrawRect = Image.FromBitmap(Properties.Resources.TestImage_GrayDrawRect); + private readonly Image testImage_GrayDrawText = Image.FromBitmap(Properties.Resources.TestImage_GrayDrawText); + private readonly Image testImage_GrayFillRect = Image.FromBitmap(Properties.Resources.TestImage_GrayFillRect); + private readonly Image testImage_GrayFillCircle = Image.FromBitmap(Properties.Resources.TestImage_GrayFillCircle); + private readonly Image testImage_GrayDrawTextWithBackground = Image.FromBitmap(Properties.Resources.TestImage_GrayDrawTextWithBackground); + private readonly Image testImage_GrayFlip = Image.FromBitmap(Properties.Resources.TestImage_GrayFlip); + private readonly Image testImage_GrayResized = Image.FromBitmap(Properties.Resources.TestImage_GrayResized); + private readonly Image testImage_GrayRotate = Image.FromBitmap(Properties.Resources.TestImage_GrayRotate); + private readonly Image testImage_GraySetPixel = Image.FromBitmap(Properties.Resources.TestImage_GraySetPixel); + private readonly Image testImage_SetPixel = Image.FromBitmap(Properties.Resources.TestImage_SetPixel); + private readonly Image testImage = Image.FromBitmap(Properties.Resources.TestImage); + private readonly Image testImage2 = Image.FromBitmap(Properties.Resources.TestImage2); + private readonly Image testImage2_Threshold = Image.FromBitmap(Properties.Resources.TestImage2_Threshold); + private readonly Image testImage2_RedChannel = Image.FromBitmap(Properties.Resources.TestImage2_RedChannel); + private readonly Image testImage2_GreenChannel = Image.FromBitmap(Properties.Resources.TestImage2_GreenChannel); + private readonly Image testImage2_BlueChannel = Image.FromBitmap(Properties.Resources.TestImage2_BlueChannel); + private readonly Image testImage2_CopyImage = Image.FromBitmap(Properties.Resources.TestImage2_CopyImage); + private readonly Image testImage2_Invert = Image.FromBitmap(Properties.Resources.TestImage2_Invert); + private readonly Image testImage2_Mask = Image.FromBitmap(Properties.Resources.TestImage2_Mask); + private readonly Image testImage2_FlipHoriz = Image.FromBitmap(Properties.Resources.TestImage2_FlipHoriz); + private readonly Image testImage2_FlipVert = Image.FromBitmap(Properties.Resources.TestImage2_FlipVert); + private readonly Image testImage2_Rotate_Neg10 = Image.FromBitmap(Properties.Resources.TestImage2_Rotate_Neg10); + private readonly Image testImage2_Rotate_Neg10_Loose = Image.FromBitmap(Properties.Resources.TestImage2_Rotate_Neg10_Loose); + private readonly Image testImage2_Rotate_110 = Image.FromBitmap(Properties.Resources.TestImage2_Rotate_110); + private readonly Image testImage2_Rotate_110_Loose = Image.FromBitmap(Properties.Resources.TestImage2_Rotate_110_Loose); + private readonly Image testImage2_DrawRect = Image.FromBitmap(Properties.Resources.TestImage2_DrawRect); + private readonly Image testImage2_DrawLine = Image.FromBitmap(Properties.Resources.TestImage2_DrawLine); + private readonly Image testImage2_DrawCircle = Image.FromBitmap(Properties.Resources.TestImage2_DrawCircle); + private readonly Image testImage2_DrawText = Image.FromBitmap(Properties.Resources.TestImage2_DrawText); + private readonly Image testImage2_FillRect = Image.FromBitmap(Properties.Resources.TestImage2_FillRect); + private readonly Image testImage2_FillCircle = Image.FromBitmap(Properties.Resources.TestImage2_FillCircle); + private readonly Image testImage2_DrawTextWithBackground = Image.FromBitmap(Properties.Resources.TestImage2_DrawTextWithBackground); + private readonly Image testImage2_AbsDiff = Image.FromBitmap(Properties.Resources.TestImage2_AbsDiff); + private readonly Image testImage_0_0_200_100 = Image.FromBitmap(Properties.Resources.TestImage_Crop_0_0_200_100); + private readonly Image testImage_153_57_103_199 = Image.FromBitmap(Properties.Resources.TestImage_Crop_153_57_103_199); + private readonly Image testImage_73_41_59_37 = Image.FromBitmap(Properties.Resources.TestImage_Crop_73_41_59_37); + private readonly Image testImage_50_25_Cubic = Image.FromBitmap(Properties.Resources.TestImage_Scale_50_25_Cubic); + private readonly Image testImage_150_125_Point = Image.FromBitmap(Properties.Resources.TestImage_Scale_150_125_Point); + private readonly Image testImage_25_200_Linear = Image.FromBitmap(Properties.Resources.TestImage_Scale_25_200_Linear); + private readonly Image solidColorsImage = Image.FromBitmap(Properties.Resources.SolidColors); [TestMethod] [Timeout(60000)] @@ -795,30 +795,26 @@ public void Image_ExtractChannels(PixelFormat pixelFormat) public void Image_CropViaOperator() { // Test that the pipeline's operator Crop() works on a stream of images and random rectangles - using (var pipeline = Pipeline.Create("CropViaOperator")) - { - var generator = Generators.Sequence(pipeline, 1, x => x + 1, 100, TimeSpan.FromTicks(1)); - var p = Microsoft.Psi.Operators.Process, System.Drawing.Rectangle)>( - generator, - (d, e, s) => + using var pipeline = Pipeline.Create("CropViaOperator"); + var generator = Generators.Sequence(pipeline, 1, x => x + 1, 100, TimeSpan.FromTicks(1)); + var p = Microsoft.Psi.Operators.Process, System.Drawing.Rectangle)>( + generator, + (d, e, s) => + { + var r = new Random(); + var rect = default(System.Drawing.Rectangle); + rect.X = r.Next() % this.testImage.Width; + rect.Y = r.Next() % this.testImage.Height; + rect.Width = r.Next() % (this.testImage.Width - rect.X); + rect.Height = r.Next() % (this.testImage.Height - rect.Y); + if (rect.Width > 0 && rect.Height > 0) { - Random r = new Random(); - System.Drawing.Rectangle rect = default(System.Drawing.Rectangle); - rect.X = r.Next() % this.testImage.Width; - rect.Y = r.Next() % this.testImage.Height; - rect.Width = r.Next() % (this.testImage.Width - rect.X); - rect.Height = r.Next() % (this.testImage.Height - rect.Y); - if (rect.Width > 0 && rect.Height > 0) - { - using (var sharedImage = ImagePool.GetOrCreate(this.testImage.Width, this.testImage.Height, this.testImage.PixelFormat)) - { - this.testImage.CopyTo(sharedImage.Resource); - s.Post((sharedImage, rect), e.OriginatingTime); - } - } - }).Crop(); - pipeline.Run(); - } + using var sharedImage = ImagePool.GetOrCreate(this.testImage.Width, this.testImage.Height, this.testImage.PixelFormat); + this.testImage.CopyTo(sharedImage.Resource); + s.Post((sharedImage, rect), e.OriginatingTime); + } + }).Crop(); + pipeline.Run(); } [TestMethod] @@ -826,35 +822,31 @@ public void Image_CropViaOperator() public void Image_CropViaJoinOperator() { // Test that the pipeline's operator Crop() works on a stream of images and random rectangles - using (var pipeline = Pipeline.Create("CropViaOperator")) - { - using (var sharedImage = ImagePool.GetOrCreate(this.testImage.Width, this.testImage.Height, this.testImage.PixelFormat)) - { - this.testImage.CopyTo(sharedImage.Resource); - - // Use a non-insignificant interval for both Sequences to ensure that the Join processes all - // messages from both streams (default interval of 1-tick is too small to guarantee this). - var images = Generators.Sequence(pipeline, sharedImage, x => sharedImage, 100, TimeSpan.FromMilliseconds(1)); - var rects = Generators.Sequence( - pipeline, - new System.Drawing.Rectangle(0, 0, 1, 1), - x => - { - Random r = new Random(); - System.Drawing.Rectangle rect = default(System.Drawing.Rectangle); - rect.X = r.Next(0, this.testImage.Width); - rect.Y = r.Next(0, this.testImage.Height); - rect.Width = r.Next(1, this.testImage.Width - rect.X); - rect.Height = r.Next(1, this.testImage.Height - rect.Y); - - return rect; - }, - 100, - TimeSpan.FromMilliseconds(1)); - images.Join(rects, Reproducible.Nearest()).Crop(); - pipeline.Run(); - } - } + using var pipeline = Pipeline.Create("CropViaOperator"); + using var sharedImage = ImagePool.GetOrCreate(this.testImage.Width, this.testImage.Height, this.testImage.PixelFormat); + this.testImage.CopyTo(sharedImage.Resource); + + // Use a non-insignificant interval for both Sequences to ensure that the Join processes all + // messages from both streams (default interval of 1-tick is too small to guarantee this). + var images = Generators.Sequence(pipeline, sharedImage, x => sharedImage, 100, TimeSpan.FromMilliseconds(1)); + var rects = Generators.Sequence( + pipeline, + new System.Drawing.Rectangle(0, 0, 1, 1), + x => + { + var r = new Random(); + var rect = default(System.Drawing.Rectangle); + rect.X = r.Next(0, this.testImage.Width); + rect.Y = r.Next(0, this.testImage.Height); + rect.Width = r.Next(1, this.testImage.Width - rect.X); + rect.Height = r.Next(1, this.testImage.Height - rect.Y); + + return rect; + }, + 100, + TimeSpan.FromMilliseconds(1)); + images.Join(rects, Reproducible.Nearest()).Crop(); + pipeline.Run(); } [TestMethod] @@ -903,13 +895,11 @@ public void Image_Crop(PixelFormat pixelFormat) public void Image_CropDifferentRegions() { // Crop a slightly different interior region of the same size and verify that the data is different (as a sanity check) - using (var croppedImage = this.testImage.Crop(74, 42, 59, 37)) - { - var croppedImage_74_42_59_37 = croppedImage; - CollectionAssert.AreNotEqual( - this.testImage_73_41_59_37.ReadBytes(this.testImage_73_41_59_37.Size), - croppedImage_74_42_59_37.ReadBytes(croppedImage_74_42_59_37.Size)); - } + using var croppedImage = this.testImage.Crop(74, 42, 59, 37); + var croppedImage_74_42_59_37 = croppedImage; + CollectionAssert.AreNotEqual( + this.testImage_73_41_59_37.ReadBytes(this.testImage_73_41_59_37.Size), + croppedImage_74_42_59_37.ReadBytes(croppedImage_74_42_59_37.Size)); } [TestMethod] @@ -1014,7 +1004,7 @@ public void Image_Compare() { this.AssertAreImagesEqual(this.testImage, this.testImage); this.AssertAreImagesEqual(this.testImage_Gray, this.testImage_Gray); - ImageError err = new ImageError(); + var err = new ImageError(); Assert.IsFalse(this.testImage2.Compare(this.testImage2_DrawRect, 2.0, 0.01, ref err)); } @@ -1068,7 +1058,7 @@ public void Image_Serialize() Serializer.Serialize(writer, this.testImage, context); // verify the image type schema - string contract = TypeSchema.GetContractName(typeof(Image), knownSerializers.RuntimeVersion); + string contract = TypeSchema.GetContractName(typeof(Image), knownSerializers.RuntimeInfo.SerializationSystemVersion); Assert.IsTrue(knownSerializers.Schemas.ContainsKey(contract)); // deserialize the image and verify the data @@ -1096,7 +1086,7 @@ public void DepthImage_Serialize() Serializer.Serialize(writer, testDepthImage, context); // verify the image type schema - string contract = TypeSchema.GetContractName(typeof(DepthImage), knownSerializers.RuntimeVersion); + string contract = TypeSchema.GetContractName(typeof(DepthImage), knownSerializers.RuntimeInfo.SerializationSystemVersion); Assert.IsTrue(knownSerializers.Schemas.ContainsKey(contract)); // deserialize the image and verify the data @@ -1150,22 +1140,20 @@ public void Image_SaveAndLoad(PixelFormat pixelFormat) sourceImage.Save(filename); // Load the image from file and compare - using (var testImage = Image.FromFile(filename)) + using var testImage = Image.FromFile(filename); + if (pixelFormat == PixelFormat.RGB_24bpp) + { + // RGB_24bpp images are converted to BGR_24bpp before saving + this.AssertAreImagesEqual(sourceImage.Convert(PixelFormat.BGR_24bpp), testImage); + } + else if (pixelFormat == PixelFormat.BGRX_32bpp) + { + // BGRX_32bpp images are converted to BGRA_32bpp before saving + this.AssertAreImagesEqual(sourceImage.Convert(PixelFormat.BGRA_32bpp), testImage); + } + else { - if (pixelFormat == PixelFormat.RGB_24bpp) - { - // RGB_24bpp images are converted to BGR_24bpp before saving - this.AssertAreImagesEqual(sourceImage.Convert(PixelFormat.BGR_24bpp), testImage); - } - else if (pixelFormat == PixelFormat.BGRX_32bpp) - { - // BGRX_32bpp images are converted to BGRA_32bpp before saving - this.AssertAreImagesEqual(sourceImage.Convert(PixelFormat.BGRA_32bpp), testImage); - } - else - { - this.AssertAreImagesEqual(sourceImage, testImage); - } + this.AssertAreImagesEqual(sourceImage, testImage); } } } @@ -1175,9 +1163,35 @@ public void Image_SaveAndLoad(PixelFormat pixelFormat) } } + [TestMethod] + [Timeout(60000)] + public void SharedImagePoolCollisionTest() + { + var bmp57 = new System.Drawing.Bitmap(5, 7); + var bmp75 = new System.Drawing.Bitmap(7, 5); + + Assert.AreEqual(5, bmp57.Width); + Assert.AreEqual(7, bmp57.Height); + Assert.AreEqual(7, bmp75.Width); + Assert.AreEqual(5, bmp75.Height); + + var shared57 = ImagePool.GetOrCreateFromBitmap(bmp57); + Assert.AreEqual(5, shared57.Resource.Width); + Assert.AreEqual(7, shared57.Resource.Height); + + // Ensure that the ImagePool is not recycling images based solely on the product of + // width*height (i.e. the same number of pixels but different dimensions), as the + // stride and total size of the recycled image could be incorrect. + + shared57.Dispose(); // release to be recycled + var shared75 = ImagePool.GetOrCreateFromBitmap(bmp75); // should *not* get the recycled image + Assert.AreEqual(7, shared75.Resource.Width); + Assert.AreEqual(5, shared75.Resource.Height); + } + private void AssertAreImagesEqual(ImageBase referenceImage, ImageBase subjectImage, double tolerance = 6.0, double percentOutliersAllowed = 0.01) { - ImageError err = new ImageError(); + var err = new ImageError(); Assert.AreEqual(referenceImage.Stride, subjectImage.Stride); // also check for consistency in the strides of allocated Images Assert.IsTrue( referenceImage.Compare(subjectImage, tolerance, percentOutliersAllowed, ref err), diff --git a/Sources/Imaging/Test.Psi.Imaging.Windows/Properties/AssemblyInfo.cs b/Sources/Imaging/Test.Psi.Imaging.Windows/Properties/AssemblyInfo.cs deleted file mode 100644 index d3c9b752d..000000000 --- a/Sources/Imaging/Test.Psi.Imaging.Windows/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. - -using System.Reflection; -using System.Runtime.InteropServices; - -[assembly: AssemblyTitle("Test.Psi.Imaging.Windows")] -[assembly: AssemblyProduct("Test.Psi.Imaging.Windows")] -[assembly: AssemblyCompany("Microsoft Corporation")] -[assembly: AssemblyCopyright("Copyright (c) Microsoft Corporation. All rights reserved.")] -[assembly: ComVisible(false)] -[assembly: Guid("191df615-3d8f-45a3-b763-dd4a604a712a")] -[assembly: AssemblyVersion("0.17.52.1")] -[assembly: AssemblyFileVersion("0.17.52.1")] -[assembly: AssemblyInformationalVersion("0.17.52.1-beta")] diff --git a/Sources/Imaging/Test.Psi.Imaging.Windows/Test.Psi.Imaging.Windows.csproj b/Sources/Imaging/Test.Psi.Imaging.Windows/Test.Psi.Imaging.Windows.csproj index b3ad25e6a..e649868d2 100644 --- a/Sources/Imaging/Test.Psi.Imaging.Windows/Test.Psi.Imaging.Windows.csproj +++ b/Sources/Imaging/Test.Psi.Imaging.Windows/Test.Psi.Imaging.Windows.csproj @@ -1,215 +1,79 @@ - - + + - Debug - AnyCPU - {191DF615-3D8F-45A3-B763-DD4A604A712A} Exe - Properties - Test.Psi.Imaging - Test.Psi.Imaging.Windows - v4.7.2 - true - 512 - {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - 10.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages - False - UnitTest - SAK - SAK - SAK - SAK - + net472 + ../../../Build/Test.Psi.ruleset - - true - full - false - bin\Debug\ - TRACE;DEBUG;CODE_ANALYSIS - prompt - 4 - ..\..\..\Build\Test.Psi.ruleset - false + + true - false - - - false + + AnyCPU + TRACE;DEBUG - - pdbonly - true - bin\Release\ - TRACE;CODE_ANALYSIS - prompt - 4 - false - ..\..\..\Build\Test.Psi.ruleset + + true - false - - + + AnyCPU + - + true + true + Test.Psi.Imaging.Windows + Test.Psi.Imaging.Windows + Test.Psi.Imaging.ConsoleMain + Test.Psi.Imaging + - - - - - - - - - - - - - - - - True - True - Resources.resx - - - - - - - - {f843dafa-a02b-4b63-8985-6890e513312e} - Test.Psi.Common - - - {04147400-0ab0-4f07-9975-d4b7e58150db} - Microsoft.Psi - - - {02a92f0e-98f1-4b42-883a-761272bac185} - Microsoft.Psi.Imaging.Windows - - - {9bf2e5ef-186a-4179-b753-ae11ee90e026} - Microsoft.Psi.Imaging - - - - - ResXFileCodeGenerator - Resources.Designer.cs - Designer - + + + - - 2.9.8 - runtime; build; native; contentfiles; analyzers + all - - - 2.1.1 - - - 2.1.1 - - - 1.1.118 runtime; build; native; contentfiles; analyzers - all - - 1.7.0 - - - 4.7.0 + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - - - - + + + + + - + + True + True + Resources.resx + + - + + ResXFileCodeGenerator + Resources.Designer.cs + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - False - - - False - - - False - - - False - - - - - - - + \ No newline at end of file diff --git a/Sources/Integrations/CognitiveServices/Microsoft.Psi.CognitiveServices.Language.Windows/Microsoft.Psi.CognitiveServices.Language.Windows.csproj b/Sources/Integrations/CognitiveServices/Microsoft.Psi.CognitiveServices.Language.Windows/Microsoft.Psi.CognitiveServices.Language.Windows.csproj index 1a5dceddc..20b85641b 100644 --- a/Sources/Integrations/CognitiveServices/Microsoft.Psi.CognitiveServices.Language.Windows/Microsoft.Psi.CognitiveServices.Language.Windows.csproj +++ b/Sources/Integrations/CognitiveServices/Microsoft.Psi.CognitiveServices.Language.Windows/Microsoft.Psi.CognitiveServices.Language.Windows.csproj @@ -29,6 +29,7 @@ all runtime; build; native; contentfiles; analyzers + diff --git a/Sources/Integrations/Onnx/Common/ModelRunners/ImageNet/ImageNetModelRunner.cs b/Sources/Integrations/Onnx/Common/ModelRunners/ImageNet/ImageNetModelRunner.cs index 8c9c8b808..1a77aa871 100644 --- a/Sources/Integrations/Onnx/Common/ModelRunners/ImageNet/ImageNetModelRunner.cs +++ b/Sources/Integrations/Onnx/Common/ModelRunners/ImageNet/ImageNetModelRunner.cs @@ -27,12 +27,12 @@ namespace Microsoft.Psi.Onnx /// downloaded locally and the path to the model file will need to be /// specified when creating the configuration. /// - public class ImageNetModelRunner : ConsumerProducer, List> + public class ImageNetModelRunner : ConsumerProducer, List>, IDisposable { private readonly ImageNetModelRunnerConfiguration configuration; private readonly float[] onnxInputVector = new float[3 * 224 * 224]; - private readonly OnnxModel onnxModel; private readonly ImageNetModelOutputParser outputParser; + private OnnxModel onnxModel; /// /// Initializes a new instance of the class. @@ -62,6 +62,13 @@ public ImageNetModelRunner(Pipeline pipeline, ImageNetModelRunnerConfiguration c this.outputParser = new ImageNetModelOutputParser(configuration.ImageClassesFilePath, configuration.ApplySoftmaxToModelOutput); } + /// + public void Dispose() + { + this.onnxModel?.Dispose(); + this.onnxModel = null; + } + /// protected override void Receive(Shared data, Envelope envelope) { diff --git a/Sources/Integrations/Onnx/Common/ModelRunners/MaskRCNN/MaskRCNNModelConfiguration.cs b/Sources/Integrations/Onnx/Common/ModelRunners/MaskRCNN/MaskRCNNModelConfiguration.cs index 28b02d3b4..ab7c36f2c 100644 --- a/Sources/Integrations/Onnx/Common/ModelRunners/MaskRCNN/MaskRCNNModelConfiguration.cs +++ b/Sources/Integrations/Onnx/Common/ModelRunners/MaskRCNN/MaskRCNNModelConfiguration.cs @@ -28,6 +28,11 @@ public class MaskRCNNModelConfiguration /// public string ClassesFileName { get; set; } + /// + /// Gets or sets the confidence threshold to use in filtering results. + /// + public float ConfidenceThreshold { get; set; } = 0.3f; + /// /// Gets or sets the GPU device ID to run execution on, or null to run on CPU. /// diff --git a/Sources/Integrations/Onnx/Common/ModelRunners/MaskRCNN/MaskRCNNModelOutputParser.cs b/Sources/Integrations/Onnx/Common/ModelRunners/MaskRCNN/MaskRCNNModelOutputParser.cs index 42dae52f7..8b7b5559e 100644 --- a/Sources/Integrations/Onnx/Common/ModelRunners/MaskRCNN/MaskRCNNModelOutputParser.cs +++ b/Sources/Integrations/Onnx/Common/ModelRunners/MaskRCNN/MaskRCNNModelOutputParser.cs @@ -18,12 +18,14 @@ internal static class MaskRCNNModelOutputParser /// /// Confidence level scores. /// Bounding boxes. + /// Scale factor applied to bounding boxes (to original image width). + /// Scale factor applied to bounding boxes (to original image height). /// Classification labels. /// Masks within bounding box. /// Classes corresponding to label indexes. /// The confidence threshold to use in filtering results. /// The list of detection results. - internal static IEnumerable Extract(float[] scores, float[] boxes, long[] labels, float[] masks, string[] classes, float confidenceThreshold = .3F) + internal static IEnumerable Extract(float[] scores, float[] boxes, float scaleWidth, float scaleHeight, long[] labels, float[] masks, string[] classes, float confidenceThreshold) { for (var i = 0; i < scores.Length; i++) { @@ -33,10 +35,10 @@ internal static IEnumerable Extract(float[] scores, float[] b var label = classes[(int)labels[i]]; var box = i * 4; - var x0 = boxes[box]; - var y0 = boxes[box + 1]; - var x1 = boxes[box + 2]; - var y1 = boxes[box + 3]; + var x0 = boxes[box] * scaleWidth; + var y0 = boxes[box + 1] * scaleHeight; + var x1 = boxes[box + 2] * scaleWidth; + var y1 = boxes[box + 3] * scaleHeight; var bounds = new RectangleF(x0, y0, x1 - x0, y1 - y0); const int MASK_SIZE = 28 * 28; diff --git a/Sources/Integrations/Onnx/Common/ModelRunners/MaskRCNN/MaskRCNNModelRunner.cs b/Sources/Integrations/Onnx/Common/ModelRunners/MaskRCNN/MaskRCNNModelRunner.cs index 7b2649b79..c438d8399 100644 --- a/Sources/Integrations/Onnx/Common/ModelRunners/MaskRCNN/MaskRCNNModelRunner.cs +++ b/Sources/Integrations/Onnx/Common/ModelRunners/MaskRCNN/MaskRCNNModelRunner.cs @@ -3,6 +3,7 @@ namespace Microsoft.Psi.Onnx { + using System; using System.IO; using Microsoft.Psi; using Microsoft.Psi.Components; @@ -24,10 +25,15 @@ namespace Microsoft.Psi.Onnx /// and the classes file is available here: /// https://github.com/onnx/models/blob/main/vision/object_detection_segmentation/mask-rcnn/dependencies/coco_classes.txt . /// - public class MaskRCNNModelRunner : ConsumerProducer, MaskRCNNDetectionResults> + public class MaskRCNNModelRunner : ConsumerProducer, MaskRCNNDetectionResults>, IDisposable { - private readonly MaskRCNNOnnxModel onnxModel; private readonly string[] classes; + private readonly float confidenceThreshold; + private readonly int imageWidth; + private readonly int imageHeight; + private readonly Func> sharedImageAllocator; + + private MaskRCNNOnnxModel onnxModel; /// /// Initializes a new instance of the class. @@ -35,19 +41,34 @@ public class MaskRCNNModelRunner : ConsumerProducer, MaskRCNNDetec /// The pipeline to add the component to. /// The name of the model file (see remarks on ). /// The name of the file containing class names (see remarks on ). - /// Image wigth (should be between 800 and 1312, inclusive, and divisible by 32). - /// Image height (should be between 800 and 1312, inclusive, and divisible by 32). + /// The confidence threshold to use in filtering results. + /// Image width to resize to before sending to Mask R-CNN (should be between 800 and 1312, inclusive, and divisible by 32). + /// Image height to resize to before sending to Mask R-CNN (should be between 800 and 1312, inclusive, and divisible by 32). /// The GPU device ID to run execution on, or null to run on CPU. + /// An optional image allocator used to create shared images for passing to MaskRCNN. /// An optional name for the component. /// /// To run on a GPU, use the Microsoft.Psi.Onnx.ModelRunners.Gpu library instead of Microsoft.Psi.Onnx.ModelRunners.Cpu, and set /// the value of the parameter to a valid non-negative integer. Typical device ID values are 0 or 1. /// - public MaskRCNNModelRunner(Pipeline pipeline, string modelFileName, string classesFileName, int imageWidth, int imageHeight, int? gpuDeviceId = null, string name = nameof(MaskRCNNModelRunner)) + public MaskRCNNModelRunner( + Pipeline pipeline, + string modelFileName, + string classesFileName, + float confidenceThreshold, + int imageWidth, + int imageHeight, + int? gpuDeviceId = null, + Func> sharedImageAllocator = null, + string name = nameof(MaskRCNNModelRunner)) : base(pipeline, name) { - this.onnxModel = new MaskRCNNOnnxModel(imageWidth, imageHeight, modelFileName, gpuDeviceId); this.classes = File.ReadAllLines(classesFileName); + this.confidenceThreshold = confidenceThreshold; + this.imageWidth = imageWidth; + this.imageHeight = imageHeight; + this.sharedImageAllocator = sharedImageAllocator ??= ImagePool.GetOrCreate; + this.onnxModel = new MaskRCNNOnnxModel(imageWidth, imageHeight, modelFileName, gpuDeviceId); } /// @@ -55,12 +76,33 @@ public MaskRCNNModelRunner(Pipeline pipeline, string modelFileName, string class /// /// The pipeline to add the component to. /// The configuration for the component. + /// Optional image allocator to create new shared image. /// An optional name for the component. - public MaskRCNNModelRunner(Pipeline pipeline, MaskRCNNModelConfiguration configuration, string name = nameof(MaskRCNNModelRunner)) - : this(pipeline, configuration.ModelFileName, configuration.ClassesFileName, configuration.ImageWidth, configuration.ImageHeight, configuration.GpuDeviceId) + public MaskRCNNModelRunner( + Pipeline pipeline, + MaskRCNNModelConfiguration configuration, + Func> sharedImageAllocator = null, + string name = nameof(MaskRCNNModelRunner)) + : this( + pipeline, + configuration.ModelFileName, + configuration.ClassesFileName, + configuration.ConfidenceThreshold, + configuration.ImageWidth, + configuration.ImageHeight, + configuration.GpuDeviceId, + sharedImageAllocator, + name) { } + /// + public void Dispose() + { + this.onnxModel.Dispose(); + this.onnxModel = null; + } + /// protected override void Receive(Shared data, Envelope envelope) { @@ -69,9 +111,12 @@ protected override void Receive(Shared data, Envelope envelope) var detections = MaskRCNNModelOutputParser.Extract( output.Scores, output.Boxes, + (float)data.Resource.Width / (float)this.imageWidth, + (float)data.Resource.Height / (float)this.imageHeight, output.Labels, output.Masks, - this.classes); + this.classes, + this.confidenceThreshold); var results = new MaskRCNNDetectionResults(detections, data.Resource.Width, data.Resource.Height); this.Out.Post(results, envelope.OriginatingTime); } @@ -82,44 +127,51 @@ protected override void Receive(Shared data, Envelope envelope) /// The shared image to construct the input vector for. private float[] ConstructOnnxInput(Shared sharedImage) { - var inputImage = sharedImage.Resource; - var inputWidth = sharedImage.Resource.Width; - var inputHeight = sharedImage.Resource.Height; - var size = 3 * inputWidth * inputHeight; + var size = 3 * this.imageWidth * this.imageHeight; - byte[] ExtractBytes() + float[] ConstructInputVectorFromBytes(byte[] bytes) { - if (sharedImage.Resource.PixelFormat != PixelFormat.BGR_24bpp) - { - // convert before extracting bytes - using var reformattedImage = ImagePool.GetOrCreate(inputWidth, inputHeight, PixelFormat.BGR_24bpp); - inputImage.CopyTo(reformattedImage.Resource); - return reformattedImage.Resource.ReadBytes(size); - } - else + var inputVector = new float[size]; + int j = 0; + + void CopyChannel(int offset, float normalization) { - return inputImage.ReadBytes(size); + for (int i = offset; i < bytes.Length; i += 3) + { + inputVector[j++] = bytes[i] - normalization; + } } - } - var bytes = ExtractBytes(); + CopyChannel(2, 102.9801f); // blue + CopyChannel(1, 115.9465f); // green + CopyChannel(0, 122.7717f); // red - var inputVector = new float[size]; - int j = 0; + return inputVector; + } - void CopyChannel(int offset, float normalization) + float[] ConstructInputVectorFromImage(Image image) { - for (int i = offset; i < bytes.Length; i += 3) + if (image.Width != this.imageWidth || image.Height != this.imageHeight) { - inputVector[j++] = bytes[i] - normalization; + // resize to match configured input size + using var resizedSharedImage = this.sharedImageAllocator(this.imageWidth, this.imageHeight, PixelFormat.BGR_24bpp); + image.Resize(resizedSharedImage.Resource, this.imageWidth, this.imageHeight, SamplingMode.Bicubic); + return ConstructInputVectorFromBytes(resizedSharedImage.Resource.ReadBytes(size)); } + + return ConstructInputVectorFromBytes(image.ReadBytes(size)); } - CopyChannel(2, 102.9801f); // blue - CopyChannel(1, 115.9465f); // green - CopyChannel(0, 122.7717f); // red + var inputImage = sharedImage.Resource; + if (inputImage.PixelFormat != PixelFormat.BGR_24bpp) + { + // convert before extracting bytes + using var reformattedImage = this.sharedImageAllocator(inputImage.Width, inputImage.Height, PixelFormat.BGR_24bpp); + inputImage.CopyTo(reformattedImage.Resource); + return ConstructInputVectorFromImage(reformattedImage.Resource); + } - return inputVector; + return ConstructInputVectorFromImage(inputImage); } } } diff --git a/Sources/Integrations/Onnx/Common/ModelRunners/MaskRCNN/MaskRCNNOnnxModel.cs b/Sources/Integrations/Onnx/Common/ModelRunners/MaskRCNN/MaskRCNNOnnxModel.cs index c963fd4ad..6854904bd 100644 --- a/Sources/Integrations/Onnx/Common/ModelRunners/MaskRCNN/MaskRCNNOnnxModel.cs +++ b/Sources/Integrations/Onnx/Common/ModelRunners/MaskRCNN/MaskRCNNOnnxModel.cs @@ -3,6 +3,7 @@ namespace Microsoft.Psi.Onnx { + using System; using System.Collections.Generic; using System.Linq; using Microsoft.ML; @@ -12,16 +13,16 @@ namespace Microsoft.Psi.Onnx /// /// Implements an ONNX model for Mask R-CNN. /// - public class MaskRCNNOnnxModel + public class MaskRCNNOnnxModel : IDisposable { private const string BOXES = "6568"; private const string LABELS = "6570"; private const string SCORES = "6572"; private const string MASKS = "6887"; - private readonly MLContext context = new MLContext(); + private readonly MLContext context = new (); private readonly SchemaDefinition schemaDefinition; - private readonly OnnxTransformer onnxTransformer; + private OnnxTransformer onnxTransformer; /// /// Initializes a new instance of the class. @@ -57,6 +58,13 @@ public MaskRCNNOnnxModel(int imageWidth, int imageHeight, string modelFileName, this.onnxTransformer = scoringEstimator.Fit(onnxEmptyInputDataView); } + /// + public void Dispose() + { + this.onnxTransformer?.Dispose(); + this.onnxTransformer = null; + } + /// /// Runs the ONNX model on an input vector. /// diff --git a/Sources/Integrations/Onnx/Common/ModelRunners/TinyYoloV2/TinyYoloV2OnnxModelRunner.cs b/Sources/Integrations/Onnx/Common/ModelRunners/TinyYoloV2/TinyYoloV2OnnxModelRunner.cs index a32d03f40..c01b4b80d 100644 --- a/Sources/Integrations/Onnx/Common/ModelRunners/TinyYoloV2/TinyYoloV2OnnxModelRunner.cs +++ b/Sources/Integrations/Onnx/Common/ModelRunners/TinyYoloV2/TinyYoloV2OnnxModelRunner.cs @@ -24,10 +24,10 @@ namespace Microsoft.Psi.Onnx /// be loaded from. The model is available in the ONNX Model Zoo at /// https://github.com/onnx/models/raw/3d4b2c28f951064ab35c89d5f5c3ffe74a149e4b/vision/object_detection_segmentation/tiny-yolov2/model/tinyyolov2-8.onnx. /// - public class TinyYoloV2OnnxModelRunner : ConsumerProducer, List> + public class TinyYoloV2OnnxModelRunner : ConsumerProducer, List>, IDisposable { private readonly float[] onnxInputVector = new float[3 * 416 * 416]; - private readonly OnnxModel onnxModel; + private OnnxModel onnxModel; /// /// Initializes a new instance of the class. @@ -55,6 +55,13 @@ public TinyYoloV2OnnxModelRunner(Pipeline pipeline, string modelFileName, int? g }); } + /// + public void Dispose() + { + this.onnxModel.Dispose(); + this.onnxModel = null; + } + /// protected override void Receive(Shared data, Envelope envelope) { diff --git a/Sources/Integrations/Onnx/Common/OnnxModel.cs b/Sources/Integrations/Onnx/Common/OnnxModel.cs index 2484f8703..2e9fe444b 100644 --- a/Sources/Integrations/Onnx/Common/OnnxModel.cs +++ b/Sources/Integrations/Onnx/Common/OnnxModel.cs @@ -3,6 +3,7 @@ namespace Microsoft.Psi.Onnx { + using System; using System.Collections.Generic; using System.Linq; using Microsoft.ML; @@ -16,12 +17,12 @@ namespace Microsoft.Psi.Onnx /// It does so by leveraging the ML.NET framework. The /// object specified at construction /// time provides information about where to load the network from, etc. - public class OnnxModel + public class OnnxModel : IDisposable { private readonly OnnxModelConfiguration configuration; - private readonly MLContext context = new MLContext(); + private readonly MLContext context = new (); private readonly SchemaDefinition schemaDefinition; - private readonly OnnxTransformer onnxTransformer; + private OnnxTransformer onnxTransformer; /// /// Initializes a new instance of the class. @@ -52,6 +53,13 @@ public OnnxModel(OnnxModelConfiguration configuration) this.onnxTransformer = scoringEstimator.Fit(onnxEmptyInputDataView); } + /// + public void Dispose() + { + this.onnxTransformer?.Dispose(); + this.onnxTransformer = null; + } + /// /// Runs the ONNX model on an input vector. /// diff --git a/Sources/Integrations/Onnx/Common/OnnxModelRunner.cs b/Sources/Integrations/Onnx/Common/OnnxModelRunner.cs index bcbfe16e7..a70680ea7 100644 --- a/Sources/Integrations/Onnx/Common/OnnxModelRunner.cs +++ b/Sources/Integrations/Onnx/Common/OnnxModelRunner.cs @@ -3,9 +3,7 @@ namespace Microsoft.Psi.Onnx { - using System.IO; using Microsoft.Psi; - using Microsoft.Psi.Components; /// /// Component that runs an ONNX model. @@ -16,19 +14,10 @@ namespace Microsoft.Psi.Onnx /// an output stream containing a vector of floats. This component is /// constructed by specifying an /// object that describes where to load the model from, and runs the - /// bare-bones model. In general, input data sent to a neural network - /// contained in an ONNX model often needs some processing (e.g., - /// image pixels need to be arranged in a specific order in the input - /// tensor, etc.), and similarly the output vector must often be post- - /// processed to obtain the desired, final results. + /// bare-bones model. /// - public class OnnxModelRunner : ConsumerProducer + public class OnnxModelRunner : OnnxModelRunner { - private readonly int inputVectorSize; - - // helper class that actually runs the ONNX model. - private readonly OnnxModel onnxModel; - /// /// Initializes a new instance of the class, based on a given configuration. /// @@ -38,26 +27,12 @@ public class OnnxModelRunner : ConsumerProducer /// The configuration parameter specifies the model filename, the /// name of the input and output vectors in that ONNX model, as well as /// the input vector size. - public OnnxModelRunner(Pipeline pipeline, OnnxModelConfiguration configuration, string name = nameof(OnnxModelRunner)) - : base(pipeline, name) - { - this.inputVectorSize = configuration.InputVectorSize; - this.onnxModel = new OnnxModel(configuration); - } - - /// - protected override void Receive(float[] data, Envelope envelope) + public OnnxModelRunner( + Pipeline pipeline, + OnnxModelConfiguration configuration, + string name = nameof(OnnxModelRunner)) + : base (pipeline, configuration, i => i, o => o, name) { - // check that the incoming data has the expected length - if (data.Length != this.inputVectorSize) - { - throw new InvalidDataException( - $"The input vector for the {nameof(OnnxModelRunner)} has size {data.Length}, which does " + - $"not match the input vector size ({this.inputVectorSize}) specified in configuration."); - } - - // run the model and post the results. - this.Out.Post(this.onnxModel.GetPrediction(data), envelope.OriginatingTime); } } } diff --git a/Sources/Integrations/Onnx/Common/OnnxModelRunner{TIn,TOut}.cs b/Sources/Integrations/Onnx/Common/OnnxModelRunner{TIn,TOut}.cs new file mode 100644 index 000000000..086a93df2 --- /dev/null +++ b/Sources/Integrations/Onnx/Common/OnnxModelRunner{TIn,TOut}.cs @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.Psi.Onnx +{ + using System; + using System.IO; + using Microsoft.Psi; + using Microsoft.Psi.Components; + + /// + /// Component that runs an ONNX model. + /// + /// The type of input messages. + /// The type of output messages. + /// + /// This class implements a \psi component that runs an ONNX model. + /// The component is constructed via an + /// object that describes where to load the model from. In general, input + /// data sent to a neural network contained in an ONNX model often needs + /// some processing (e.g., image pixels need to be arranged in a specific + /// order in the input tensor, etc.), and similarly the output vector must + /// often be post-processed to obtain the desired, final results. This + /// component allows the user to provide this input and output processing + /// functions in the constructor. + /// + public class OnnxModelRunner : ConsumerProducer, IDisposable + { + private readonly int inputVectorSize; + private readonly Func inputConstructor; + private readonly Func outputConstructor; + + // helper class that actually runs the ONNX model. + private OnnxModel onnxModel; + + /// + /// Initializes a new instance of the class, based on a given configuration. + /// + /// The pipeline to add the component to. + /// The component configuration. + /// A function that constructs the network input (float array) from the component input. + /// A function that constructs the component output from the network output (float array). + /// An optional name for the component. + /// The configuration parameter specifies the model filename, the name of the input and output vectors in + /// that ONNX model, as well as the input vector size. + public OnnxModelRunner( + Pipeline pipeline, + OnnxModelConfiguration configuration, + Func inputConstructor, + Func outputConstuctor, + string name = nameof(OnnxModelRunner)) + : base(pipeline, name) + { + this.inputVectorSize = configuration.InputVectorSize; + this.inputConstructor = inputConstructor; + this.outputConstructor = outputConstuctor; + this.onnxModel = new OnnxModel(configuration); + } + + /// + public void Dispose() + { + this.onnxModel?.Dispose(); + this.onnxModel = null; + } + + /// + protected override void Receive(TIn input, Envelope envelope) + { + // construct the input vector + var inputVector = this.inputConstructor(input); + + // check that the incoming data has the expected length + if (inputVector.Length != this.inputVectorSize) + { + throw new InvalidDataException( + $"The input vector for the {nameof(OnnxModelRunner)} has size {inputVector.Length}, which does " + + $"not match the input vector size ({this.inputVectorSize}) specified in configuration."); + } + + // run the model, construct the output, and post the results + var outputVector = this.onnxModel.GetPrediction(inputVector); + var output = this.outputConstructor(outputVector); + this.Out.Post(output, envelope.OriginatingTime); + } + } +} diff --git a/Sources/Integrations/Onnx/Test.Psi.Onnx/Test.Psi.Onnx.csproj b/Sources/Integrations/Onnx/Test.Psi.Onnx/Test.Psi.Onnx.csproj index 97a7a9c21..3ace54c51 100644 --- a/Sources/Integrations/Onnx/Test.Psi.Onnx/Test.Psi.Onnx.csproj +++ b/Sources/Integrations/Onnx/Test.Psi.Onnx/Test.Psi.Onnx.csproj @@ -22,6 +22,7 @@ + diff --git a/Sources/Media/Microsoft.Psi.Media.Linux/FFMPEGFrameInfo.cs b/Sources/Media/Microsoft.Psi.Media.Linux/FFMPEGFrameInfo.cs index f4310a3a7..3fbaa055f 100644 --- a/Sources/Media/Microsoft.Psi.Media.Linux/FFMPEGFrameInfo.cs +++ b/Sources/Media/Microsoft.Psi.Media.Linux/FFMPEGFrameInfo.cs @@ -3,30 +3,30 @@ #if FFMPEG -namespace Microsoft.Psi.Media.Native.Linux +namespace Microsoft.Psi.Media { /// - /// Contains information about the current video/audio frame + /// Contains information about the current video/audio frame. /// public class FFMPEGFrameInfo { /// - /// Constant used to define frame data associated with the video stream + /// Constant used to define frame data associated with the video stream. /// public const int FrameTypeVideo = 0; /// - /// Constant used to define frame data associated with the audio stream + /// Constant used to define frame data associated with the audio stream. /// public const int FrameTypeAudio = 1; /// - /// Gets or sets the type of frame data + /// Gets or sets the type of frame data. /// public int FrameType { get; set; } // Type of data to be returned next by call to ReadFrameData /// - /// Gets or sets the buffer size of the current frame + /// Gets or sets the buffer size of the current frame. /// public int BufferSize { get; set; } // The size of the buffer required to hold the decompressed data } diff --git a/Sources/Media/Shared/FFMPEGMediaSource.cs b/Sources/Media/Microsoft.Psi.Media.Linux/FFMPEGMediaSource.cs similarity index 91% rename from Sources/Media/Shared/FFMPEGMediaSource.cs rename to Sources/Media/Microsoft.Psi.Media.Linux/FFMPEGMediaSource.cs index d50520847..731b31421 100644 --- a/Sources/Media/Shared/FFMPEGMediaSource.cs +++ b/Sources/Media/Microsoft.Psi.Media.Linux/FFMPEGMediaSource.cs @@ -2,20 +2,15 @@ // Licensed under the MIT license. #if FFMPEG + namespace Microsoft.Psi.Media { using System; - using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using Microsoft.Psi.Audio; using Microsoft.Psi.Components; using Microsoft.Psi.Imaging; -#if WINDOWS - using Microsoft.Psi.Media.Native.Windows; -#else - using Microsoft.Psi.Media.Native.Linux; -#endif /// /// Component that streams video and audio from an MPEG file. @@ -38,14 +33,14 @@ public class FFMPEGMediaSource : Generator, IDisposable /// Initializes a new instance of the class. /// /// The pipeline to add the component to. - /// Name of media file to play - /// Output format for images + /// Name of media file to play. + /// Output format for images. /// An optional name for the component. public FFMPEGMediaSource(Pipeline pipeline, string filename, PixelFormat format = PixelFormat.BGRX_32bpp, string name = nameof(FFMPEGMediaSource)) : base(pipeline, name: name) { FileInfo info = new FileInfo(filename); - pipeline.ProposeReplayTime(new TimeInterval(info.CreationTime, DateTime.MaxValue), new TimeInterval(info.CreationTime, DateTime.MaxValue)); + pipeline.ProposeReplayTime(new TimeInterval(info.CreationTime, DateTime.MaxValue)); this.start = info.CreationTime; this.filename = filename; this.Image = pipeline.CreateEmitter>(this, nameof(this.Image)); @@ -60,7 +55,7 @@ public FFMPEGMediaSource(Pipeline pipeline, string filename, PixelFormat format } /// - /// Gets the emitter that generates images from the media + /// Gets the emitter that generates images from the media. /// public int Width { @@ -71,7 +66,7 @@ public int Width } /// - /// Gets the emitter that generates images from the media + /// Gets the emitter that generates images from the media. /// public int Height { @@ -82,17 +77,17 @@ public int Height } /// - /// Gets the emitter that generates images from the media + /// Gets the emitter that generates images from the media. /// public Emitter> Image { get; private set; } /// - /// Gets the emitter that generates audio from the media + /// Gets the emitter that generates audio from the media. /// public Emitter Audio { get; private set; } /// - /// Releases the media player + /// Releases the media player. /// public void Dispose() { @@ -103,24 +98,24 @@ public void Dispose() } /// - /// GenerateNext is called by the Generator base class when the next sample should be read + /// GenerateNext is called by the Generator base class when the next sample should be read. /// - /// Time of previous sample - /// Time for current sample + /// Time of previous sample. + /// Time for current sample. protected override DateTime GenerateNext(DateTime previous) { DateTime originatingTime = default(DateTime); FFMPEGFrameInfo frameInfo = new FFMPEGFrameInfo(); bool eos = false; bool frameRead = this.mpegReader.NextFrame(ref frameInfo, out eos); - if (!frameRead) + if (eos) { - return this.lastAudioTime; + return DateTime.MaxValue; } - if (eos) + if (!frameRead) { - return DateTime.MaxValue; + return this.lastAudioTime; } double timestamp = 0.0; diff --git a/Sources/Media/Microsoft.Psi.Media.Linux/FFMPEGReader.cs b/Sources/Media/Microsoft.Psi.Media.Linux/FFMPEGReader.cs index 0032b2be5..d6fc660a4 100644 --- a/Sources/Media/Microsoft.Psi.Media.Linux/FFMPEGReader.cs +++ b/Sources/Media/Microsoft.Psi.Media.Linux/FFMPEGReader.cs @@ -2,14 +2,14 @@ // Licensed under the MIT license. #if FFMPEG -#pragma warning disable SA1615, SA1600 -namespace Microsoft.Psi.Media.Native.Linux + +namespace Microsoft.Psi.Media { using System; using System.Runtime.InteropServices; /// - /// Defines our wrapper class for calling into our Native FFMPEG reader + /// Defines our wrapper class for calling into our Native FFMPEG reader. /// public class FFMPEGReader { @@ -18,10 +18,10 @@ public class FFMPEGReader /// /// Initializes a new instance of the class. /// - /// Depth of requested output images. Must be 24 or 32 + /// Depth of requested output images. Must be 24 or 32. public FFMPEGReader(int imageDepth) { - this.unmanagedData = FFMPEGReaderNative_Alloc(imageDepth); + this.unmanagedData = NativeMethods.FFMPEGReaderNative_Alloc(imageDepth); } /// @@ -31,107 +31,74 @@ public FFMPEGReader(int imageDepth) { if (this.unmanagedData != IntPtr.Zero) { - FFMPEGReaderNative_Dealloc(this.unmanagedData); + NativeMethods.FFMPEGReaderNative_Dealloc(this.unmanagedData); this.unmanagedData = IntPtr.Zero; } } /// - /// Gets the width of the current video frame + /// Gets the width of the current video frame. /// public int Width { get { - return (this.unmanagedData != IntPtr.Zero) ? FFMPEGReaderNative_GetWidth(this.unmanagedData) : 0; + return (this.unmanagedData != IntPtr.Zero) ? NativeMethods.FFMPEGReaderNative_GetWidth(this.unmanagedData) : 0; } } /// - /// Gets the height of the current video frame + /// Gets the height of the current video frame. /// public int Height { get { - return (this.unmanagedData != IntPtr.Zero) ? FFMPEGReaderNative_GetHeight(this.unmanagedData) : 0; + return (this.unmanagedData != IntPtr.Zero) ? NativeMethods.FFMPEGReaderNative_GetHeight(this.unmanagedData) : 0; } } /// - /// Gets the audio sample rate + /// Gets the audio sample rate. /// public int AudioSampleRate { get { - return (this.unmanagedData != IntPtr.Zero) ? FFMPEGReaderNative_GetAudioSampleRate(this.unmanagedData) : 0; + return (this.unmanagedData != IntPtr.Zero) ? NativeMethods.FFMPEGReaderNative_GetAudioSampleRate(this.unmanagedData) : 0; } } /// - /// Gets the audio bits per sample + /// Gets the audio bits per sample. /// public int AudioBitsPerSample { get { - return (this.unmanagedData != IntPtr.Zero) ? FFMPEGReaderNative_GetAudioBitsPerSample(this.unmanagedData) : 0; + return (this.unmanagedData != IntPtr.Zero) ? NativeMethods.FFMPEGReaderNative_GetAudioBitsPerSample(this.unmanagedData) : 0; } } /// - /// Gets the number of audio channels + /// Gets the number of audio channels. /// public int AudioNumChannels { get { - return (this.unmanagedData != IntPtr.Zero) ? FFMPEGReaderNative_GetAudioNumChannels(this.unmanagedData) : 0; + return (this.unmanagedData != IntPtr.Zero) ? NativeMethods.FFMPEGReaderNative_GetAudioNumChannels(this.unmanagedData) : 0; } } - [DllImport("Microsoft.Psi.Media.Native.so", EntryPoint="FFMPEGReaderNative_Alloc")] - public static extern IntPtr FFMPEGReaderNative_Alloc(int imageDepth); - - [DllImport("Microsoft.Psi.Media.Native.so", EntryPoint="FFMPEGReaderNative_Dealloc")] - public static extern void FFMPEGReaderNative_Dealloc(IntPtr obj); - - [DllImport("Microsoft.Psi.Media.Native.so", EntryPoint="FFMPEGReaderNative_GetWidth")] - public static extern int FFMPEGReaderNative_GetWidth(IntPtr obj); - - [DllImport("Microsoft.Psi.Media.Native.so", EntryPoint="FFMPEGReaderNative_GetHeight")] - public static extern int FFMPEGReaderNative_GetHeight(IntPtr obj); - - [DllImport("Microsoft.Psi.Media.Native.so", EntryPoint="FFMPEGReaderNative_GetAudioSampleRate")] - public static extern int FFMPEGReaderNative_GetAudioSampleRate(IntPtr obj); - - [DllImport("Microsoft.Psi.Media.Native.so", EntryPoint="FFMPEGReaderNative_GetAudioBitsPerSample")] - public static extern int FFMPEGReaderNative_GetAudioBitsPerSample(IntPtr obj); - - [DllImport("Microsoft.Psi.Media.Native.so", EntryPoint="FFMPEGReaderNative_GetAudioNumChannels")] - public static extern int FFMPEGReaderNative_GetAudioNumChannels(IntPtr obj); - - [DllImport("Microsoft.Psi.Media.Native.so", EntryPoint="FFMPEGReaderNative_Open", CharSet=CharSet.Ansi)] - public static extern int FFMPEGReaderNative_Open(IntPtr obj, [MarshalAs(UnmanagedType.LPStr)]string fn); - - [DllImport("Microsoft.Psi.Media.Native.so", EntryPoint="FFMPEGReaderNative_NextFrame")] - public static extern int FFMPEGReaderNative_NextFrame(IntPtr obj, ref int frameType, ref int requiredBufferSize, ref bool eos); - - [DllImport("Microsoft.Psi.Media.Native.so", EntryPoint="FFMPEGReaderNative_ReadFrameData")] - public static extern int FFMPEGReaderNative_ReadFrameData(IntPtr obj, IntPtr buffer, ref int bytesRead, ref double timestamp); - - [DllImport("Microsoft.Psi.Media.Native.so", EntryPoint="FFMPEGReaderNative_Close")] - public static extern int FFMPEGReaderNative_Close(IntPtr obj); - /// /// Opens a MP4 file for writing. /// - /// File to open - /// Configuration + /// File to open. + /// Configuration. public void Open(string fn, FFMPEGReaderConfiguration config) { - int hr = FFMPEGReaderNative_Open(this.unmanagedData, fn); + int hr = NativeMethods.FFMPEGReaderNative_Open(this.unmanagedData, fn); if (hr < 0) { throw new Exception("Failed to read video frame. HRESULT=" + hr.ToString()); @@ -147,16 +114,16 @@ public void Open(string fn, FFMPEGReaderConfiguration config) /// read by the client via a call to ReadFrameData(). /// Returns true if a frame was read; false otherwise. /// - /// Filled with info about the next frame - /// Returns true if end of stream detected - /// false if error detected. true otherwise + /// Filled with info about the next frame. + /// Returns true if end of stream detected. + /// false if error detected. true otherwise. public bool NextFrame(ref FFMPEGFrameInfo info, out bool endOfStream) { int frameType = 0; int requiredBufferSize = 0; bool eos = false; endOfStream = false; - int hr = FFMPEGReaderNative_NextFrame(this.unmanagedData, ref frameType, ref requiredBufferSize, ref eos); + int hr = NativeMethods.FFMPEGReaderNative_NextFrame(this.unmanagedData, ref frameType, ref requiredBufferSize, ref eos); if (hr == 1) { return false; @@ -183,17 +150,17 @@ public bool NextFrame(ref FFMPEGFrameInfo info, out bool endOfStream) /// 'dataBuffer' will be filled with the decompressed data. The buffer /// is allocated and controlled by the calling client. The size of the required /// buffer was returned by the client's previous call to NextFrame(). - /// Return true if we successfully decoded a frame + /// Return true if we successfully decoded a frame. /// - /// Buffer to fill with data - /// Size of buffer in bytes - /// Timestamp associated with the data - /// false if error detected. true otherwise + /// Buffer to fill with data. + /// Size of buffer in bytes. + /// Timestamp associated with the data. + /// false if error detected. true otherwise. public bool ReadFrameData(IntPtr dataBuffer, ref int bufferSize, ref double timestampMillisecs) { double ts = 0.0; int bytesRead = 0; - int hr = FFMPEGReaderNative_ReadFrameData(this.unmanagedData, dataBuffer, ref bytesRead, ref ts); + int hr = NativeMethods.FFMPEGReaderNative_ReadFrameData(this.unmanagedData, dataBuffer, ref bytesRead, ref ts); if (hr < 0) { throw new Exception("Failed to read video frame. HRESULT=" + hr.ToString()); @@ -210,15 +177,15 @@ public bool ReadFrameData(IntPtr dataBuffer, ref int bufferSize, ref double time } /// - /// Close the reader + /// Close the reader. /// public void Close() { int hr = 0; if (this.unmanagedData != IntPtr.Zero) { - hr = FFMPEGReaderNative_Close(this.unmanagedData); - FFMPEGReaderNative_Dealloc(this.unmanagedData); + hr = NativeMethods.FFMPEGReaderNative_Close(this.unmanagedData); + NativeMethods.FFMPEGReaderNative_Dealloc(this.unmanagedData); this.unmanagedData = IntPtr.Zero; } @@ -227,7 +194,42 @@ public void Close() throw new Exception("Failed to read video frame. HRESULT=" + hr.ToString()); } } + + private static class NativeMethods + { + [DllImport("Microsoft.Psi.Media_Interop.so", EntryPoint = "FFMPEGReaderNative_Alloc")] + public static extern IntPtr FFMPEGReaderNative_Alloc(int imageDepth); + + [DllImport("Microsoft.Psi.Media_Interop.so", EntryPoint = "FFMPEGReaderNative_Dealloc")] + public static extern void FFMPEGReaderNative_Dealloc(IntPtr obj); + + [DllImport("Microsoft.Psi.Media_Interop.so", EntryPoint = "FFMPEGReaderNative_GetWidth")] + public static extern int FFMPEGReaderNative_GetWidth(IntPtr obj); + + [DllImport("Microsoft.Psi.Media_Interop.so", EntryPoint = "FFMPEGReaderNative_GetHeight")] + public static extern int FFMPEGReaderNative_GetHeight(IntPtr obj); + + [DllImport("Microsoft.Psi.Media_Interop.so", EntryPoint = "FFMPEGReaderNative_GetAudioSampleRate")] + public static extern int FFMPEGReaderNative_GetAudioSampleRate(IntPtr obj); + + [DllImport("Microsoft.Psi.Media_Interop.so", EntryPoint = "FFMPEGReaderNative_GetAudioBitsPerSample")] + public static extern int FFMPEGReaderNative_GetAudioBitsPerSample(IntPtr obj); + + [DllImport("Microsoft.Psi.Media_Interop.so", EntryPoint = "FFMPEGReaderNative_GetAudioNumChannels")] + public static extern int FFMPEGReaderNative_GetAudioNumChannels(IntPtr obj); + + [DllImport("Microsoft.Psi.Media_Interop.so", EntryPoint = "FFMPEGReaderNative_Open", BestFitMapping = false, ThrowOnUnmappableChar = true)] + public static extern int FFMPEGReaderNative_Open(IntPtr obj, [MarshalAs(UnmanagedType.LPStr)] string fn); + + [DllImport("Microsoft.Psi.Media_Interop.so", EntryPoint = "FFMPEGReaderNative_NextFrame")] + public static extern int FFMPEGReaderNative_NextFrame(IntPtr obj, ref int frameType, ref int requiredBufferSize, ref bool eos); + + [DllImport("Microsoft.Psi.Media_Interop.so", EntryPoint = "FFMPEGReaderNative_ReadFrameData")] + public static extern int FFMPEGReaderNative_ReadFrameData(IntPtr obj, IntPtr buffer, ref int bytesRead, ref double timestamp); + + [DllImport("Microsoft.Psi.Media_Interop.so", EntryPoint = "FFMPEGReaderNative_Close")] + public static extern int FFMPEGReaderNative_Close(IntPtr obj); + } } } -#pragma warning restore SA1615, SA1600 #endif // FFMPEG diff --git a/Sources/Media/Microsoft.Psi.Media.Linux/FFMPEGReaderConfiguration.cs b/Sources/Media/Microsoft.Psi.Media.Linux/FFMPEGReaderConfiguration.cs index ae7a8fe03..0a6efbe78 100644 --- a/Sources/Media/Microsoft.Psi.Media.Linux/FFMPEGReaderConfiguration.cs +++ b/Sources/Media/Microsoft.Psi.Media.Linux/FFMPEGReaderConfiguration.cs @@ -3,10 +3,10 @@ #if FFMPEG -namespace Microsoft.Psi.Media.Native.Linux +namespace Microsoft.Psi.Media { /// - /// Defines configuration parameters for FFMPEG + /// Defines configuration parameters for FFMPEG. /// public class FFMPEGReaderConfiguration { diff --git a/Sources/Media/Microsoft.Psi.Media.Linux/Microsoft.Psi.Media.Linux.csproj b/Sources/Media/Microsoft.Psi.Media.Linux/Microsoft.Psi.Media.Linux.csproj index 9db34b853..02e9f5109 100644 --- a/Sources/Media/Microsoft.Psi.Media.Linux/Microsoft.Psi.Media.Linux.csproj +++ b/Sources/Media/Microsoft.Psi.Media.Linux/Microsoft.Psi.Media.Linux.csproj @@ -35,9 +35,6 @@ - - - all diff --git a/Sources/Media/Microsoft.Psi.Media.Linux/Readme.md b/Sources/Media/Microsoft.Psi.Media.Linux/Readme.md new file mode 100644 index 000000000..5c8cec4ec --- /dev/null +++ b/Sources/Media/Microsoft.Psi.Media.Linux/Readme.md @@ -0,0 +1,19 @@ +# Additional requirements for FFmpeg support + +The `Microsoft.Psi.Media.Linux` library includes optional support for a basic FFmpeg-based MPEG-4 file reader and media source component. By default, this functionality is not included when building the project. In order to enable it, see the following sections. + +## FFmpeg Version +FFmpeg support currently depends on Version 3.x of FFmpeg (tested against FFmpeg 3.4.11 on Ubuntu 18.04 LTS). Building the interop library requires both the FFmpeg runtime and development files. These may be obtained either by building FFmpeg [from source](http://ffmpeg.org/releases/), or by installing the appropriate development packages, e.g.: + - Debian: [libavdevice-dev](https://packages.debian.org/stretch/libavdevice-dev) (installs all required dependencies) + - Ubuntu: [libavdevice-dev](https://packages.ubuntu.com/bionic/libavdevice-dev) (installs all required dependencies) + + +## Environment Variable +An environment variable `FFMPEGDir` needs to be defined in your build environment to point to the location of the FFmpeg libraries. This should be the path to the lib directory in which the various FFmpeg `lib*.so` files are located. You can set this in your bash shell before building, e.g.: +``` +export $FFMPEGDir=/usr/lib/x86_64-linux-gnu +./build.sh +``` + +## Native Interop Library +In addition, you will need to build the native interop project located in the `../Microsoft.Psi.Media_Interop.Linux/` directory. Make sure that you have followed the previous step to set the `FFMPEGDir` variable to point to the location of the FFmpeg libraries before building the interop library. This should produce a shared library `../Microsoft.Psi.Media_Interop.Linux/bin/Microsoft.Psi.Media_Interop.so` which will then need to be copied to your runtime directory (i.e. the same location as your application executable). diff --git a/Sources/Media/Microsoft.Psi.Media.Native.x64/Makefile b/Sources/Media/Microsoft.Psi.Media.Native.x64/Makefile deleted file mode 100644 index 00c9fa148..000000000 --- a/Sources/Media/Microsoft.Psi.Media.Native.x64/Makefile +++ /dev/null @@ -1,21 +0,0 @@ -FFMPEGLibDir=$(FFMPEGDir) -FFMpegLibs=$(FFMPEGLibDir)/libavdevice/libavdevice.so\ - $(FFMPEGLibDir)/libavfilter/libavfilter.so\ - $(FFMPEGLibDir)/libswresample/libswresample.so\ - $(FFMPEGLibDir)/libavcodec/libavcodec.so\ - $(FFMPEGLibDir)/libavformat/libavformat.so\ - $(FFMPEGLibDir)/libavutil/libavutil.so\ - $(FFMPEGLibDir)/libswscale/libswscale.so -FFMpegIncludes=$(FFMPEGDir) -FFMpegDefines=-DUSE_FFMPEG -DLINUX -SOURCES=\ - FFMPEGReaderNative.o - -Microsoft.Psi.Media.Native.so: $(SOURCES) - g++ -g -shared -Wl,-Bsymbolic -o $@ $(SOURCES) $(FFMpegLibs) - -%.o: %.cpp - g++ -g -fPIC -pthread -std=c++11 -c -o $@ $< -I$(FFMpegIncludes) $(FFMpegDefines) -Wno-deprecated-declarations - -clean: - rm $(SOURCES) diff --git a/Sources/Media/Microsoft.Psi.Media.Native.x64/Microsoft.Psi.Media.Native.x64.cpp b/Sources/Media/Microsoft.Psi.Media.Native.x64/Microsoft.Psi.Media.Native.x64.cpp deleted file mode 100644 index b37c828542f9e60f59362b49eb14559a7dc5a1cc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 258 zcmYL@K?=e!5Jmr5@D4%u%|=|f?$U}^D5;?ZF=A86>D4zukzq0~lRxwGetCI0b($3P zP|{3BWi>j(6SdPt1JuJO&I~{4H7eR`M-93YP6x8(C8D@dXYN@e - - - - Debug - x64 - - - Release - x64 - - - - 15.0 - {C50F7F21-BEB0-4366-B73F-859EEBC3ED42} - Win32Proj - MicrosoftPsiMediaNativex64 - 10.0.19041.0 - 4.7.2 - - - $(FFMPEGDir)\lib\avdevice.lib;$(FFMPEGDir)\lib\avfilter.lib;$(FFMPEGDir)\lib\postproc.lib;$(FFMPEGDir)\lib\swresample.lib;$(FFMPEGDir)\lib\avcodec.lib;$(FFMPEGDir)\lib\avformat.lib;$(FFMPEGDir)\lib\avutil.lib;$(FFMPEGDir)\lib\swscale.lib - $(FFMPEGDir)\include - USE_FFMPEG - - - - - - - - - DynamicLibrary - true - v142 - Unicode - Spectre - - - DynamicLibrary - false - v142 - true - Unicode - Spectre - - - - - - - - - - - - - - - false - $(IncludePath);$(FFMpegIncludes) - $(Platform)\$(Configuration)\ - $(ProjectDir)\$(Platform)\$(Configuration)\ - - - false - $(IncludePath);$(FFMpegIncludes) - $(Platform)\$(Configuration)\ - $(ProjectDir)\$(Platform)\$(Configuration)\ - - - - Use - Level3 - Disabled - true - _DEBUG;MICROSOFTPSIMEDIANATIVEX64_EXPORTS;_WINDOWS;_USRDLL;$(FFMpegDefines);%(PreprocessorDefinitions) - true - Guard - ProgramDatabase - - - Windows - true - $(FFMpegLibs);%(AdditionalDependencies) - - - - - Use - Level3 - MaxSpeed - true - true - true - NDEBUG;MICROSOFTPSIMEDIANATIVEX64_EXPORTS;_WINDOWS;_USRDLL;$(FFMpegDefines);%(PreprocessorDefinitions) - true - Guard - - - Windows - true - true - true - $(FFMpegLibs);%(AdditionalDependencies) - - - - - - - - - - - - - Create - Create - - - - - - \ No newline at end of file diff --git a/Sources/Media/Microsoft.Psi.Media.Native.x64/Microsoft.Psi.Media.Native.x64.vcxproj.filters b/Sources/Media/Microsoft.Psi.Media.Native.x64/Microsoft.Psi.Media.Native.x64.vcxproj.filters deleted file mode 100644 index f7b0df8f4..000000000 --- a/Sources/Media/Microsoft.Psi.Media.Native.x64/Microsoft.Psi.Media.Native.x64.vcxproj.filters +++ /dev/null @@ -1,45 +0,0 @@ - - - - - {4FC737F1-C7A5-4376-A066-2A32D752A2FF} - cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx - - - {93995380-89BD-4b04-88EB-625FBE52EBFB} - h;hh;hpp;hxx;hm;inl;inc;xsd - - - {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} - rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms - - - - - Header Files - - - Header Files - - - Source Files - - - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - - - - \ No newline at end of file diff --git a/Sources/Media/Microsoft.Psi.Media.Native.x64/dllmain.cpp b/Sources/Media/Microsoft.Psi.Media.Native.x64/dllmain.cpp deleted file mode 100644 index 1637303289629f1b0f4e14b8d2b76eeb49cea00c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 912 zcmbu7%}T>i5QWcL@Ew9~Du~_ds+vLztx8N4SE05k5o1%5)E~rGSHHPQ!6-shhTG1a zoH;XR=Dxpm)s?5YQtVz+-D;@=&9uTi9ddldTDW+uf_tfih{svt=X%CpDRYPte}SKW z!uc9u9^A&6f}7!&{AaG#wzjnEvS)l<(~Gh1P^-kB!uLc+2@yluIo26woIRausJ>1W zD$q3%-zgc_N;*Ua8iV#VA*0Vd;hw>~1{rJLF?z(?o3ASv12`?mcp?_=os`)H)F?Q)9{8RAvhzKMe`#bDdSvn?eI_XBw9-=XY^K%Zv&~wE zwg=mKgoXeOCrthVi`ZC*^@t33UAD~))A!sEOh9H2w*O@MaI~%Ywcnp}H+|pKyT8*> bL(gmDX78h9zBzB+bFXzmS5-!Zv{`%r^3-`d diff --git a/Sources/Media/Microsoft.Psi.Media.Native.x64/stdafx.cpp b/Sources/Media/Microsoft.Psi.Media.Native.x64/stdafx.cpp deleted file mode 100644 index db2c3d2d6b01acf9f974df8fb71e649454c68fc7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 636 zcmZ{i!Aiqm5QOI}_zpqOwiof>O%Um&D%gY9*fb<1CLvALzP$R)hL9GK@Mkys&+g97 z=I48^xf1QvD^sdmUz%yHmM76|+%(#&BevlySE(wzUIqA`SgD%Z3RK3;j>s0@6%$So zXn&>P+@s_DH|tzyFfo@U71y8|YBY50Vf2h&VwY6Q@KXH8=wIrC*bLq`y2iHT7F78J zRq-Tx3kD;TmdYDlfok*)dYH$I%ruP|I!$a&m1(H9J*npn0}^r97A?)0En6ki;vHSE|K4fBk6YhS{B`hU#&7Zvqw3IG5A diff --git a/Sources/Media/Microsoft.Psi.Media.Native.x64/stdafx.h b/Sources/Media/Microsoft.Psi.Media.Native.x64/stdafx.h deleted file mode 100644 index d989cc726..000000000 --- a/Sources/Media/Microsoft.Psi.Media.Native.x64/stdafx.h +++ /dev/null @@ -1,18 +0,0 @@ -#ifndef LINUX -// stdafx.h : include file for standard system include files, -// or project specific include files that are used frequently, but -// are changed infrequently -// - -#pragma once - -#include "targetver.h" - -#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers -// Windows Header Files: -#include - - - -// TODO: reference additional headers your program requires here -#endif diff --git a/Sources/Media/Microsoft.Psi.Media.Native.x64/targetver.h b/Sources/Media/Microsoft.Psi.Media.Native.x64/targetver.h deleted file mode 100644 index 567cd346efccbe2d1f43a4056bdcb58a2b93e1a8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 630 zcmaiyOKZYV5QWcL=zj>fEfw0WxN;+co0fK2Vs4B9U*sm0{`t1wOo&Foy0~*EXI^K{ z&F{}p2USW{Xp2p>*G`#oJ!s%(q!H-M(Ty4fmG}kNtEQTB%)V1m=}BwwfWPvrT#@e@ zH0NG}74Ao{glS)#QXA|NYdIfY7hrMp+Ji@H`t9kzWx_SD6;~Ri;cvXH)6CO{-I1p_IJPQ#X=r zigZeSqQguJz35q;zt9^Q_C^^>*mmuXUCp&pw^fPoGX+dho4RCrySs7j@9_UipWkA5 OQDt4mH~x-^Z~X_?9czaG diff --git a/Sources/Media/Microsoft.Psi.Media.Windows.x64/Microsoft.Psi.Media.Windows.x64.csproj b/Sources/Media/Microsoft.Psi.Media.Windows.x64/Microsoft.Psi.Media.Windows.x64.csproj index 98d5b0540..ed5c9bdfc 100644 --- a/Sources/Media/Microsoft.Psi.Media.Windows.x64/Microsoft.Psi.Media.Windows.x64.csproj +++ b/Sources/Media/Microsoft.Psi.Media.Windows.x64/Microsoft.Psi.Media.Windows.x64.csproj @@ -22,18 +22,12 @@ true TRACE;DEBUG;NET47 - - $(DefineConstants);FFMPEG;WINDOWS - - - - all @@ -41,7 +35,6 @@ - diff --git a/Sources/Media/Microsoft.Psi.Media.Windows.x64/Mpeg4Writer.cs b/Sources/Media/Microsoft.Psi.Media.Windows.x64/Mpeg4Writer.cs index c798f4da9..3aa5bd696 100644 --- a/Sources/Media/Microsoft.Psi.Media.Windows.x64/Mpeg4Writer.cs +++ b/Sources/Media/Microsoft.Psi.Media.Windows.x64/Mpeg4Writer.cs @@ -4,6 +4,7 @@ namespace Microsoft.Psi.Media { using System; + using System.Collections.Generic; using Microsoft.Psi; using Microsoft.Psi.Audio; using Microsoft.Psi.Imaging; @@ -14,10 +15,11 @@ namespace Microsoft.Psi.Media /// public class Mpeg4Writer : IConsumer>, IDisposable { - private readonly Pipeline pipeline; private readonly string name; - private readonly string filename; private readonly Mpeg4WriterConfiguration configuration; + private readonly Queue<(DateTime Timestamp, AudioBuffer Audio)> audioBuffers = new (); + private readonly Queue<(DateTime Timestamp, Shared Image)> images = new (); + private IntPtr waveFmtPtr = default; private MP4Writer writer; /// @@ -67,12 +69,25 @@ public Mpeg4Writer(Pipeline pipeline, string filename, uint width, uint height, private Mpeg4Writer(Pipeline pipeline, string filename, string name) { - pipeline.PipelineRun += (s, e) => this.OnPipelineRun(); - this.pipeline = pipeline; + pipeline.PipelineRun += (_, _) => + { + MP4Writer.Startup(); + this.writer = new MP4Writer(); + this.writer.Open(filename, this.configuration.Config); + }; + + pipeline.PipelineCompleted += (_, _) => + { + if (this.writer != null) + { + // Write any remaining data + this.WriteData(DateTime.MaxValue); + } + }; + this.name = name; this.ImageIn = pipeline.CreateReceiver>(this, this.ReceiveImage, nameof(this.ImageIn)); this.AudioIn = pipeline.CreateReceiver(this, this.ReceiveAudio, nameof(this.AudioIn)); - this.filename = filename; } /// @@ -95,6 +110,11 @@ private Mpeg4Writer(Pipeline pipeline, string filename, string name) /// public void Dispose() { + if (this.waveFmtPtr != default) + { + System.Runtime.InteropServices.Marshal.FreeHGlobal(this.waveFmtPtr); + } + // check for null since it's possible that Start was never called if (this.writer != null) { @@ -105,35 +125,87 @@ public void Dispose() } } - /// - /// Called once all the subscriptions are established. - /// - private void OnPipelineRun() + /// + public override string ToString() => this.name; + + private void ReceiveImage(Shared image, Envelope envelope) { - MP4Writer.Startup(); - this.writer = new MP4Writer(); - this.writer.Open(this.filename, this.configuration.Config); + // Cache the incoming images + this.images.Enqueue((envelope.OriginatingTime, image.AddRef())); + + // Write what we can so far + if (this.writer != null) + { + this.WriteData(this.ComputeHighWaterMark()); + } } - private void ReceiveImage(Shared image, Envelope e) + private void ReceiveAudio(AudioBuffer audioBuffer, Envelope envelope) { + // Cache the incoming audio buffers + this.audioBuffers.Enqueue((envelope.OriginatingTime, audioBuffer.DeepClone())); + + // Write what we can so far if (this.writer != null) { - this.writer.WriteVideoFrame(e.OriginatingTime.Ticks, image.Resource.ImageData, (uint)image.Resource.Width, (uint)image.Resource.Height, (int)image.Resource.PixelFormat); + this.WriteData(this.ComputeHighWaterMark()); } } - private void ReceiveAudio(AudioBuffer audioBuffer, Envelope env) + private DateTime ComputeHighWaterMark() { - if (this.writer != null) + if (this.configuration.ContainsAudio) { - System.IntPtr waveFmtPtr = System.Runtime.InteropServices.Marshal.AllocHGlobal((int)WaveFormat.MarshalSizeOf(audioBuffer.Format) + sizeof(int)); - WaveFormat.MarshalToPtr(audioBuffer.Format, waveFmtPtr); - System.IntPtr audioData = System.Runtime.InteropServices.Marshal.AllocHGlobal(audioBuffer.Length); - System.Runtime.InteropServices.Marshal.Copy(audioBuffer.Data, 0, audioData, audioBuffer.Length); - this.writer.WriteAudioSample(env.OriginatingTime.Ticks, audioData, (uint)audioBuffer.Length, waveFmtPtr); - System.Runtime.InteropServices.Marshal.FreeHGlobal(waveFmtPtr); - System.Runtime.InteropServices.Marshal.FreeHGlobal(audioData); + var lastAudioTime = this.AudioIn.LastEnvelope.OriginatingTime; + var lastImageTime = this.ImageIn.LastEnvelope.OriginatingTime; + return lastAudioTime < lastImageTime ? lastAudioTime : lastImageTime; + } + else + { + return DateTime.MaxValue; + } + } + + private void WriteData(DateTime highWaterMark) + { + // Write images and audio in a coordinated way, using a watermark approach, so one stream does not get too far ahead of the other + var messageProcessed = true; + while (messageProcessed) + { + messageProcessed = false; + while (this.images.Count > 0 && + this.images.Peek().Timestamp <= highWaterMark && + (this.audioBuffers.Count == 0 || this.images.Peek().Timestamp <= this.audioBuffers.Peek().Timestamp)) + { + var image = this.images.Dequeue(); + this.writer.WriteVideoFrame(image.Timestamp.Ticks, image.Image.Resource.ImageData, (uint)image.Image.Resource.Width, (uint)image.Image.Resource.Height, (int)image.Image.Resource.PixelFormat); + image.Image.Dispose(); + messageProcessed = true; + } + + while (this.audioBuffers.Count > 0 && + this.audioBuffers.Peek().Timestamp <= highWaterMark && + (this.images.Count == 0 || this.audioBuffers.Peek().Timestamp <= this.images.Peek().Timestamp)) + { + var audio = this.audioBuffers.Dequeue(); + var audioBuffer = audio.Audio; + var audioData = System.Runtime.InteropServices.Marshal.AllocHGlobal(audioBuffer.Length); + System.Runtime.InteropServices.Marshal.Copy(audioBuffer.Data, 0, audioData, audioBuffer.Length); + + if (this.waveFmtPtr == default) + { + this.waveFmtPtr = System.Runtime.InteropServices.Marshal.AllocHGlobal((int)WaveFormat.MarshalSizeOf(audioBuffer.Format) + sizeof(int)); + WaveFormat.MarshalToPtr(audioBuffer.Format, this.waveFmtPtr); + } + + // The semantics of MP4Writer.WriteAudioSample(ticks, audio, ...) are that ticks + // represents the *start* of the audio buffer, while \psi semantics are that + // AudioBuffer messages have originating times representing the *end* of the buffer. + var originatingTicksOfStartOfAudioBuffer = audio.Timestamp.Ticks - audio.Audio.Duration.Ticks; + this.writer.WriteAudioSample(originatingTicksOfStartOfAudioBuffer, audioData, (uint)audioBuffer.Length, this.waveFmtPtr); + System.Runtime.InteropServices.Marshal.FreeHGlobal(audioData); + messageProcessed = true; + } } } } diff --git a/Sources/Media/Microsoft.Psi.Media.Windows.x64/Mpeg4WriterConfiguration.cs b/Sources/Media/Microsoft.Psi.Media.Windows.x64/Mpeg4WriterConfiguration.cs index 1a34ca39c..c65d4e569 100644 --- a/Sources/Media/Microsoft.Psi.Media.Windows.x64/Mpeg4WriterConfiguration.cs +++ b/Sources/Media/Microsoft.Psi.Media.Windows.x64/Mpeg4WriterConfiguration.cs @@ -25,6 +25,7 @@ public class Mpeg4WriterConfiguration AudioBitsPerSample = 16, AudioSamplesPerSecond = 48000, AudioChannels = 2, + DisableThrottling = false, }; /// @@ -187,6 +188,22 @@ public uint AudioChannels } } + /// + /// Gets or sets a value indicating whether the MP4 writer should throttle the incoming data. + /// + public bool DisableThrottling + { + get + { + return this.Config.disableThrottling; + } + + set + { + this.Config.disableThrottling = value; + } + } + /// /// Gets or sets the native MP4Writer's configuration object. /// diff --git a/Sources/Media/Microsoft.Psi.Media.Native.x64/FFMPEGReaderNative.cpp b/Sources/Media/Microsoft.Psi.Media_Interop.Linux/FFMPEGReaderNative.cpp similarity index 98% rename from Sources/Media/Microsoft.Psi.Media.Native.x64/FFMPEGReaderNative.cpp rename to Sources/Media/Microsoft.Psi.Media_Interop.Linux/FFMPEGReaderNative.cpp index b90f9d5fe..4c2ba1131 100644 --- a/Sources/Media/Microsoft.Psi.Media.Native.x64/FFMPEGReaderNative.cpp +++ b/Sources/Media/Microsoft.Psi.Media_Interop.Linux/FFMPEGReaderNative.cpp @@ -1,12 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -#include "stdafx.h" -#ifdef USE_FFMPEG +#if defined(LINUX) && defined(USE_FFMPEG) #include "FFMPEGReaderNative.h" -#include -#include -#include #pragma warning(push) #pragma warning(disable:4996) @@ -14,7 +10,6 @@ namespace Microsoft { namespace Psi { namespace Media { namespace Native { -namespace Windows { extern "C" { void *FFMPEGReaderNative_Alloc(int imageDepth) @@ -527,14 +522,11 @@ extern "C" { } return S_OK; } -}}}}} +}}}} #pragma warning(pop) -#else // USE_FFMPEG -#ifdef _WINDOWS -__declspec(dllexport) -#endif +#else // LINUX && USE_FFMPEG int DummyFunctionSoLibGetsGenerated() { return 0; } -#endif // USE_FFMPEG +#endif // LINUX && USE_FFMPEG diff --git a/Sources/Media/Microsoft.Psi.Media.Native.x64/FFMPEGReaderNative.h b/Sources/Media/Microsoft.Psi.Media_Interop.Linux/FFMPEGReaderNative.h similarity index 98% rename from Sources/Media/Microsoft.Psi.Media.Native.x64/FFMPEGReaderNative.h rename to Sources/Media/Microsoft.Psi.Media_Interop.Linux/FFMPEGReaderNative.h index 6dd3d8be3..cfba7d098 100644 --- a/Sources/Media/Microsoft.Psi.Media.Native.x64/FFMPEGReaderNative.h +++ b/Sources/Media/Microsoft.Psi.Media_Interop.Linux/FFMPEGReaderNative.h @@ -3,7 +3,7 @@ #pragma once -#ifdef USE_FFMPEG +#if defined(LINUX) && defined(USE_FFMPEG) #pragma warning(push) #pragma warning(disable:4634 4635 4244 4996) @@ -33,7 +33,6 @@ namespace Microsoft { namespace Psi { namespace Media { namespace Native { -namespace Windows { #define PSIERR_BUFFER_TOO_SMALL MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, 1) #define PSIERR_BSF_NOT_FOUND MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, 2) #define PSIERR_BUG MAKE_HRESULT(SEVERITY_ERROR, FACILITY_ITF, 3) @@ -102,5 +101,5 @@ namespace Windows { int GetAudioBitsPerSample(); int GetAudioNumChannels(); }; -}}}}} -#endif // USE_FFMPEG +}}}} +#endif // LINUX && USE_FFMPEG diff --git a/Sources/Media/Microsoft.Psi.Media_Interop.Linux/Makefile b/Sources/Media/Microsoft.Psi.Media_Interop.Linux/Makefile new file mode 100644 index 000000000..02cfe254b --- /dev/null +++ b/Sources/Media/Microsoft.Psi.Media_Interop.Linux/Makefile @@ -0,0 +1,30 @@ +FFMPEGLibDir=$(FFMPEGDir) +FFMPEGLibs=$(FFMPEGLibDir)/libavdevice.so\ + $(FFMPEGLibDir)/libavfilter.so\ + $(FFMPEGLibDir)/libswresample.so\ + $(FFMPEGLibDir)/libavcodec.so\ + $(FFMPEGLibDir)/libavformat.so\ + $(FFMPEGLibDir)/libavutil.so\ + $(FFMPEGLibDir)/libswscale.so +FFMPEGIncludes=$(FFMPEGDir) +FFMPEGDefines=-DUSE_FFMPEG -DLINUX + +SOURCES=$(wildcard *.cpp) +OBJS=$(patsubst %.cpp, $(OBJDIR)/%.o, $(SOURCES)) +TARGET=$(OUTDIR)/Microsoft.Psi.Media_Interop.so + +OBJDIR=obj +OUTDIR=bin +DIRS=$(OBJDIR) $(OUTDIR) + +$(TARGET): $(OBJS) | $(DIRS) + g++ -g -shared -Wl,-Bsymbolic -o $@ $(OBJS) $(FFMPEGLibs) + +$(OBJDIR)/%.o: %.cpp | $(DIRS) + g++ -g -fPIC -pthread -std=c++11 -c -o $@ $< -I$(FFMPEGIncludes) $(FFMPEGDefines) -Wno-deprecated-declarations + +$(DIRS): + mkdir -p $@ + +clean: + rm -f $(OBJS) $(TARGET) diff --git a/Sources/Media/Microsoft.Psi.Media_Interop.Linux/Readme.md b/Sources/Media/Microsoft.Psi.Media_Interop.Linux/Readme.md new file mode 100644 index 000000000..396309f56 --- /dev/null +++ b/Sources/Media/Microsoft.Psi.Media_Interop.Linux/Readme.md @@ -0,0 +1,6 @@ +This folder contains the media interop library for Linux. To build this library, simply run the bash script: +``` +./build.sh +``` + +Note that this project currently only contains optional limited support for FFmpeg, so building it is skipped if the `FFMPEGDir` environment variable is not defined. For more details on enabling FFmpeg support, see the [Readme](../Microsoft.Psi.Media.Linux/Readme.md) in the Microsoft.Psi.Media.Linux project. \ No newline at end of file diff --git a/Sources/Media/Microsoft.Psi.Media.Native.x64/build.sh b/Sources/Media/Microsoft.Psi.Media_Interop.Linux/build.sh old mode 100755 new mode 100644 similarity index 91% rename from Sources/Media/Microsoft.Psi.Media.Native.x64/build.sh rename to Sources/Media/Microsoft.Psi.Media_Interop.Linux/build.sh index ab3f080f3..728c6f1b3 --- a/Sources/Media/Microsoft.Psi.Media.Native.x64/build.sh +++ b/Sources/Media/Microsoft.Psi.Media_Interop.Linux/build.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash if [[ -z "${FFMPEGDir}" ]]; then - echo "FFMPEGDir Environment Variable Not Defined. Skipping Microsoft.Psi.Media.Native.x64" + echo "FFMPEGDir Environment Variable Not Defined. Skipping Microsoft.Psi.Media_Interop.Linux" # Future implementation might consider finding the library's path instead of needing to be predefined. # If install using the package manager, the libs are located at /usr/lib/x86_64-linux-gnu/ and headers # are at /usr/include/x86_64-linux-gnu/ diff --git a/Sources/Media/Microsoft.Psi.Media_Interop.Windows.x64/AssemblyInfo.cpp b/Sources/Media/Microsoft.Psi.Media_Interop.Windows.x64/AssemblyInfo.cpp index a8edf8299..2b2a6a69d 100644 --- a/Sources/Media/Microsoft.Psi.Media_Interop.Windows.x64/AssemblyInfo.cpp +++ b/Sources/Media/Microsoft.Psi.Media_Interop.Windows.x64/AssemblyInfo.cpp @@ -15,6 +15,6 @@ using namespace System::Security::Permissions; [assembly:AssemblyCopyrightAttribute("Copyright (c) Microsoft Corporation. All rights reserved.")]; [assembly:ComVisible(false)]; [assembly:CLSCompliantAttribute(true)]; -[assembly:AssemblyVersionAttribute("0.17.52.1")]; -[assembly:AssemblyFileVersionAttribute("0.17.52.1")]; -[assembly:AssemblyInformationalVersionAttribute("0.17.52.1-beta")]; +[assembly:AssemblyVersionAttribute("0.18.72.1")]; +[assembly:AssemblyFileVersionAttribute("0.18.72.1")]; +[assembly:AssemblyInformationalVersionAttribute("0.18.72.1-beta")]; diff --git a/Sources/Media/Microsoft.Psi.Media_Interop.Windows.x64/AssemblyInfo.rc b/Sources/Media/Microsoft.Psi.Media_Interop.Windows.x64/AssemblyInfo.rc index c90806db6ef5222e0062fc5abb5f73f97d813b31..62fba1455faf8629320d387dd42f2e3664fbc38a 100644 GIT binary patch delta 79 zcmeyMzC(RO9tXDtgARi^gAtH4oNUNpJb4X=5UT})9)tPj!yM(zlN -#include -#include "FFMPEGReader.h" - -#pragma warning(push) -#pragma warning(disable:4996) -using namespace System::Runtime::InteropServices; - -struct AVDictionary -{ - int count; - AVDictionaryEntry *elems; -}; -namespace Microsoft { - namespace Psi { - namespace Media { - namespace Native { - namespace Windows { - //********************************************************************** - // Opens a MP4 file for writing. - //********************************************************************** - void FFMPEGReader::Open(String ^fn, FFMPEGReaderConfiguration^ /*config*/) - { - IntPtr ptrToNativeString = Marshal::StringToHGlobalUni(fn); - std::wstring wstr(static_cast(ptrToNativeString.ToPointer())); - std::string str(wstr.begin(), wstr.end()); - HRESULT hr = unmanagedData->Open((char*)str.c_str()); - Marshal::FreeHGlobal(ptrToNativeString); - if (FAILED(hr)) - { - char buffer[512]; - sprintf(buffer, "Failed to read video frame. HRESULT=0x%x", hr); - throw gcnew Exception(gcnew System::String(buffer)); - } - } - - //********************************************************************** - // NextFrame() advances the playback engine to the next audio or video - // packet to be processed. This method will fill in 'info' with the type - // of packet we are about to process (FrameType), the presentation time - // stamp for the frame (Timestamp), and the size of the buffer required - // to hold the decompressed data (BufferSize). The actual data is then - // read by the client via a call to ReadFrameData(). - // Returns true if a frame was read; false otherwise. - //********************************************************************** - bool FFMPEGReader::NextFrame(FFMPEGFrameInfo ^%info, [Out] bool %endOfStream) - { - int frameType; - int requiredBufferSize; - bool eos = false; - HRESULT hr = unmanagedData->NextFrame(&frameType, &requiredBufferSize, &eos); - if (hr == S_FALSE) - { - return false; - } - if (eos) - { - endOfStream = true; - return false; - } - info->FrameType = frameType; - info->BufferSize = requiredBufferSize; - if (FAILED(hr)) - { - char buffer[512]; - sprintf(buffer, "Failed to read video frame. HRESULT=0x%x", hr); - throw gcnew Exception(gcnew System::String(buffer)); - } - return true; - } - - //********************************************************************** - // ReadFrameData() reads the next video or audio frame from the stream. - // 'dataBuffer' will be filled with the decompressed data. The buffer - // is allocated and controlled by the calling client. The size of the required - // buffer was returned by the client's previous call to NextFrame(). - // Return true if we successfully decoded a frame - //********************************************************************** - bool FFMPEGReader::ReadFrameData(IntPtr dataBuffer, int %bufferSize, double %timestampMillisecs) - { - double ts; - int bytesRead; - HRESULT hr = unmanagedData->ReadFrameData((byte*)dataBuffer.ToPointer(), &bytesRead, &ts); - if (FAILED(hr)) - { - char buffer[512]; - sprintf(buffer, "Failed to read video frame. HRESULT=0x%x", hr); - throw gcnew Exception(gcnew System::String(buffer)); - } - if (hr != S_FALSE) - { - timestampMillisecs = ts; - bufferSize = bytesRead; - return true; // Successfully decoded frame - } - return false; - } - - //********************************************************************** - void FFMPEGReader::Close() - { - HRESULT hr = S_OK; - if (unmanagedData != nullptr) - { - hr = unmanagedData->Close(); - delete unmanagedData; - unmanagedData = nullptr; - } - if (FAILED(hr)) - { - char buffer[512]; - sprintf(buffer, "Failed to read video frame. HRESULT=0x%x", hr); - throw gcnew Exception(gcnew System::String(buffer)); - } - } - } - } - } - } -} -#pragma warning(pop) -#endif // USE_FFMPEG diff --git a/Sources/Media/Microsoft.Psi.Media_Interop.Windows.x64/FFMPEGReader.h b/Sources/Media/Microsoft.Psi.Media_Interop.Windows.x64/FFMPEGReader.h deleted file mode 100644 index 04a256202..000000000 --- a/Sources/Media/Microsoft.Psi.Media_Interop.Windows.x64/FFMPEGReader.h +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. - -#pragma once - -#ifdef USE_FFMPEG -#include "Managed.h" - -#pragma warning(push) -#pragma warning(disable:4634 4635 4244 4996) -extern "C" { -#include -#include -#include -#include -#include -#include -} -#include -#pragma warning(pop) -#include "FFMPEGReaderNative.h" - -namespace Microsoft { - namespace Psi { - namespace Media { - namespace Native { - namespace Windows { - /// - /// Class for configuring our MPEG4 Reader - /// - public ref class FFMPEGReaderConfiguration - { - public: - }; - - /// - /// Class used for returning what time of data is about to - /// read by ReadFrameData(). - /// - public ref class FFMPEGFrameInfo - { - public: - static int FrameTypeVideo = 0; - static int FrameTypeAudio = 1; - property int FrameType; // Type of data to be returned next by call to ReadFrameData - property int BufferSize; // The size of the buffer required to hold the decompressed data - }; - - /// - /// Class for playing back MPEG files via FFMPEG - /// - public ref class FFMPEGReader - { - private: - FFMPEGReaderNative * unmanagedData; - public: - FFMPEGReader(int imageDepth) : - unmanagedData(nullptr) - { - unmanagedData = new FFMPEGReaderNative(); - unmanagedData->Initialize(imageDepth); - } - - ~FFMPEGReader() - { - if (unmanagedData != nullptr) - { - delete unmanagedData; - unmanagedData = nullptr; - } - } - - property int Width - { - int get() { return (unmanagedData == nullptr) ? 0 : unmanagedData->GetWidth(); } - } - - property int Height - { - int get() { return (unmanagedData == nullptr) ? 0 : unmanagedData->GetHeight(); } - } - - property int AudioSampleRate - { - int get() { return (unmanagedData == nullptr) ? 0 : unmanagedData->GetAudioSampleRate(); } - } - - property int AudioBitsPerSample - { - int get() { return (unmanagedData == nullptr) ? 0 : unmanagedData->GetAudioBitsPerSample(); } - } - - property int AudioNumChannels - { - int get() { return (unmanagedData == nullptr) ? 0 : unmanagedData->GetAudioNumChannels(); } - } - - void Open(String ^fn, FFMPEGReaderConfiguration^ config); - void Close(); - bool NextFrame(FFMPEGFrameInfo ^%info, [Out] bool %eos); - bool ReadFrameData(IntPtr dataBuffer, int %bufferSize, double %timestamp); - }; - - } - } - } - } -} -#endif // USE_FFMPEG diff --git a/Sources/Media/Microsoft.Psi.Media_Interop.Windows.x64/MP4Writer.cpp b/Sources/Media/Microsoft.Psi.Media_Interop.Windows.x64/MP4Writer.cpp index e871a4d7e..d965b3153 100644 --- a/Sources/Media/Microsoft.Psi.Media_Interop.Windows.x64/MP4Writer.cpp +++ b/Sources/Media/Microsoft.Psi.Media_Interop.Windows.x64/MP4Writer.cpp @@ -112,10 +112,11 @@ namespace Microsoft { // bitsPerSample - Bits per audio sample for the input audio. Typically this is 16. // samplesPerSample - Bitrate of the input audio. Typically this is 48000. // numChannels - Number of audio channels. This is assumed to be either 1 or 2. + // disableThrottling - Prevents the sink writer from throttling the input data. // outputFilename - name of the output file for the generated .mp4 file //********************************************************************** HRESULT MP4WriterUnmanagedData::Open(UINT32 imageWidth, UINT32 imageHeight, UINT32 frameRateNum, UINT32 frameRateDenom, UINT32 bitrate, int pixelFormat, - bool containsAudio, UINT32 bitsPerSample, UINT32 samplesPerSecond, UINT32 numChannels, + bool containsAudio, UINT32 bitsPerSample, UINT32 samplesPerSecond, UINT32 numChannels, bool disableThrottling, wchar_t *outputFilename) { hasAudio = containsAudio; @@ -139,7 +140,11 @@ namespace Microsoft { } HRESULT hr = S_OK; - IFS(MFCreateSinkWriterFromURL(outputFilename, nullptr, nullptr, &writer)); + + CComPtr attributes; + IFS(MFCreateAttributes(&attributes, 1)); + IFS(attributes->SetUINT32(MF_SINK_WRITER_DISABLE_THROTTLING, disableThrottling)); + IFS(MFCreateSinkWriterFromURL(outputFilename, nullptr, attributes, &writer)); // Define our output media type IFS(MFCreateMediaType(&outputMediaType)); @@ -411,7 +416,7 @@ namespace Microsoft { unmanagedData = new MP4WriterUnmanagedData(); IntPtr ptrToNativeString = Marshal::StringToHGlobalUni(fn); HRESULT hr = unmanagedData->Open(config->imageWidth, config->imageHeight, config->frameRateNumerator, config->frameRateDenominator, config->targetBitrate, - config->pixelFormat, config->containsAudio, config->bitsPerSample, config->samplesPerSecond, config->numChannels, + config->pixelFormat, config->containsAudio, config->bitsPerSample, config->samplesPerSecond, config->numChannels, config->disableThrottling, static_cast(ptrToNativeString.ToPointer())); Marshal::FreeHGlobal(ptrToNativeString); return hr; diff --git a/Sources/Media/Microsoft.Psi.Media_Interop.Windows.x64/MP4Writer.h b/Sources/Media/Microsoft.Psi.Media_Interop.Windows.x64/MP4Writer.h index dbc190d52..f639244fb 100644 --- a/Sources/Media/Microsoft.Psi.Media_Interop.Windows.x64/MP4Writer.h +++ b/Sources/Media/Microsoft.Psi.Media_Interop.Windows.x64/MP4Writer.h @@ -19,7 +19,7 @@ namespace Microsoft { //********************************************************************** // Defines list of native pixel formats. NOTE: This list must match // the list in Microsoft.Psi.Imaging.PixelFormats. The reason for - // the duplication is to avoid taking a dependency in Media.Native.Windows + // the duplication is to avoid taking a dependency in Media_Interop.Windows // on the Imaging layer. //********************************************************************** const int NativePixelFormat_Undefined = 0; @@ -62,7 +62,7 @@ namespace Microsoft { public: MP4WriterUnmanagedData(); HRESULT Open(UINT32 imageWidth, UINT32 imageHeight, UINT32 frameRateNum, UINT32 frameRateDenom, UINT32 bitrate, int pixelFormat, - bool containsAudio, UINT32 bitsPerSample, UINT32 samplesPerSecond, UINT32 numChannels, + bool containsAudio, UINT32 bitsPerSample, UINT32 samplesPerSecond, UINT32 numChannels, bool disableThrottling, wchar_t *outputFilename); HRESULT WriteVideoFrame(LONGLONG timestamp, IntPtr imageData, UINT32 imageWidth, UINT32 imageHeight, int pixelFormat); HRESULT WriteAudioSample(LONGLONG timestamp, IntPtr pcmData, UINT32 numDataBytes, IntPtr waveFormat); @@ -85,6 +85,7 @@ namespace Microsoft { UINT32 bitsPerSample; /* Number of bits per audio sample (typically 16) */ UINT32 samplesPerSecond; /* Audio's sample rate (typically 48000) */ UINT32 numChannels; /* Number of audio channels (typically 1 or 2) */ + bool disableThrottling; /* Whether the MP4 writer should disable throttling */ }; /// diff --git a/Sources/Media/Microsoft.Psi.Media_Interop.Windows.x64/Microsoft.Psi.Media_Interop.Windows.x64.vcxproj b/Sources/Media/Microsoft.Psi.Media_Interop.Windows.x64/Microsoft.Psi.Media_Interop.Windows.x64.vcxproj index 6d917eb5f..9bebf43be 100644 --- a/Sources/Media/Microsoft.Psi.Media_Interop.Windows.x64/Microsoft.Psi.Media_Interop.Windows.x64.vcxproj +++ b/Sources/Media/Microsoft.Psi.Media_Interop.Windows.x64/Microsoft.Psi.Media_Interop.Windows.x64.vcxproj @@ -18,16 +18,6 @@ Microsoft.Psi.Media_Interop.Windows.x64 10.0.19041.0 - - $(FFMPEGDir)\lib\avdevice.lib;$(FFMPEGDir)\lib\avfilter.lib;$(FFMPEGDir)\lib\postproc.lib;$(FFMPEGDir)\lib\swresample.lib;$(FFMPEGDir)\lib\avcodec.lib;$(FFMPEGDir)\lib\avformat.lib;$(FFMPEGDir)\lib\avutil.lib;$(FFMPEGDir)\lib\swscale.lib - $(FFMPEGDir)\include - USE_FFMPEG - - - - - - DynamicLibrary @@ -63,7 +53,7 @@ $(Platform)\$(Configuration)\ $(ProjectDir)\$(Platform)\$(Configuration)\ ..\..\..\Build\Microsoft.Psi.ruleset - $(VC_IncludePath);$(WindowsSDK_IncludePath);..\Microsoft.Psi.Media.Native.x64;$(FFMpegIncludes) + $(VC_IncludePath);$(WindowsSDK_IncludePath) false @@ -73,13 +63,13 @@ $(Platform)\$(Configuration)\ $(ProjectDir)\$(Platform)\$(Configuration)\ ..\..\..\Build\Microsoft.Psi.ruleset - $(VC_IncludePath);$(WindowsSDK_IncludePath);$(FFMpegIncludes);..\Microsoft.Psi.Media.Native.x64 + $(VC_IncludePath);$(WindowsSDK_IncludePath) Level4 Disabled - WIN32;_DEBUG;_WINDLL;$(FFMpegDefines);%(PreprocessorDefinitions) + WIN32;_DEBUG;_WINDLL;%(PreprocessorDefinitions) Use %(AdditionalIncludeDirectories) MultiThreadedDebugDLL @@ -90,7 +80,7 @@ true - $(FFMpegLibs);mfplat.lib;mf.lib;mfreadwrite.lib;mfuuid.lib;shlwapi.lib;%(AdditionalDependencies) + mfplat.lib;mf.lib;mfreadwrite.lib;mfuuid.lib;shlwapi.lib;%(AdditionalDependencies) $(SolutionDir)bin\ MachineX64 @@ -98,7 +88,7 @@ Level4 - WIN32;NDEBUG;_WINDLL;$(FFMpegDefines);%(PreprocessorDefinitions) + WIN32;NDEBUG;_WINDLL;%(PreprocessorDefinitions) Use MaxSpeed $(WindowsSDK_IncludePath);%(AdditionalIncludeDirectories) @@ -110,7 +100,7 @@ true - $(FFMpegLibs);mfplat.lib;mf.lib;mfreadwrite.lib;mfuuid.lib;shlwapi.lib;%(AdditionalDependencies) + mfplat.lib;mf.lib;mfreadwrite.lib;mfuuid.lib;shlwapi.lib;%(AdditionalDependencies) $(SolutionDir)bin\ MachineX64 @@ -120,7 +110,6 @@ - @@ -139,7 +128,6 @@ - @@ -154,28 +142,6 @@ - - - {c50f7f21-beb0-4366-b73f-859eebc3ed42} - - - - - true - Microsoft.Psi.Media.Native.x64.dll - PreserveNewest - - - true - Microsoft.Psi.Media.Native.x64.lib - PreserveNewest - - - true - Microsoft.Psi.Media.Native.x64.pdb - PreserveNewest - - diff --git a/Sources/MixedReality/HoloLensCapture/HoloLensCapture.sln b/Sources/MixedReality/HoloLensCapture/HoloLensCapture.sln index dca76e5f7..31d490cd8 100644 --- a/Sources/MixedReality/HoloLensCapture/HoloLensCapture.sln +++ b/Sources/MixedReality/HoloLensCapture/HoloLensCapture.sln @@ -44,8 +44,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Psi.Media.Windows EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Microsoft.Psi.Media_Interop.Windows.x64", "..\..\Media\Microsoft.Psi.Media_Interop.Windows.x64\Microsoft.Psi.Media_Interop.Windows.x64.vcxproj", "{5348A94F-7B3A-4B42-8555-2A1491971090}" EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Microsoft.Psi.Media.Native.x64", "..\..\Media\Microsoft.Psi.Media.Native.x64\Microsoft.Psi.Media.Native.x64.vcxproj", "{C50F7F21-BEB0-4366-B73F-859EEBC3ED42}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -122,10 +120,6 @@ Global {5348A94F-7B3A-4B42-8555-2A1491971090}.Debug|Any CPU.Build.0 = Debug|x64 {5348A94F-7B3A-4B42-8555-2A1491971090}.Release|Any CPU.ActiveCfg = Release|x64 {5348A94F-7B3A-4B42-8555-2A1491971090}.Release|Any CPU.Build.0 = Release|x64 - {C50F7F21-BEB0-4366-B73F-859EEBC3ED42}.Debug|Any CPU.ActiveCfg = Debug|x64 - {C50F7F21-BEB0-4366-B73F-859EEBC3ED42}.Debug|Any CPU.Build.0 = Debug|x64 - {C50F7F21-BEB0-4366-B73F-859EEBC3ED42}.Release|Any CPU.ActiveCfg = Release|x64 - {C50F7F21-BEB0-4366-B73F-859EEBC3ED42}.Release|Any CPU.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -148,7 +142,6 @@ Global {5318B3E0-2FEA-4DED-B041-6F7CC1E15890} = {18171A33-C859-41E1-9FB4-0E314C2B16E3} {B6EBA51D-7A78-4343-9F98-E146FAF405E4} = {18171A33-C859-41E1-9FB4-0E314C2B16E3} {5348A94F-7B3A-4B42-8555-2A1491971090} = {18171A33-C859-41E1-9FB4-0E314C2B16E3} - {C50F7F21-BEB0-4366-B73F-859EEBC3ED42} = {18171A33-C859-41E1-9FB4-0E314C2B16E3} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {EAF15EE9-DCC5-411B-A9E5-7C2F3D132331} diff --git a/Sources/MixedReality/HoloLensCapture/HoloLensCaptureApp/HoloLensCaptureApp.cs b/Sources/MixedReality/HoloLensCapture/HoloLensCaptureApp/HoloLensCaptureApp.cs index d6f832beb..2081eea7e 100644 --- a/Sources/MixedReality/HoloLensCapture/HoloLensCaptureApp/HoloLensCaptureApp.cs +++ b/Sources/MixedReality/HoloLensCapture/HoloLensCaptureApp/HoloLensCaptureApp.cs @@ -21,12 +21,19 @@ namespace HoloLensCaptureApp using Microsoft.Psi.Interop.Serialization; using Microsoft.Psi.Interop.Transport; using Microsoft.Psi.MixedReality; + using Microsoft.Psi.MixedReality.MediaCapture; + using Microsoft.Psi.MixedReality.ResearchMode; + using Microsoft.Psi.MixedReality.StereoKit; + using Microsoft.Psi.MixedReality.WinRT; using Microsoft.Psi.Remoting; using Microsoft.Psi.Spatial.Euclidean; using StereoKit; using Windows.Storage; using Color = System.Drawing.Color; - using Microphone = Microsoft.Psi.MixedReality.Microphone; + using Microphone = Microsoft.Psi.MixedReality.StereoKit.Microphone; + using OpenXRHandsSensor = Microsoft.Psi.MixedReality.OpenXR.HandsSensor; + using StereoKitHeadSensor = Microsoft.Psi.MixedReality.StereoKit.HeadSensor; + using WinRTGazeSensor = Microsoft.Psi.MixedReality.WinRT.GazeSensor; /// /// Capture app used to stream sensor data to the accompanying HoloLensCaptureServer. @@ -83,9 +90,9 @@ public class HoloLensCaptureApp private static readonly bool IncludeHead = true; private static readonly TimeSpan HeadInterval = TimeSpan.FromMilliseconds(20); private static readonly bool IncludeEyes = true; - private static readonly TimeSpan EyesInterval = TimeSpan.FromMilliseconds(20); + private static readonly TimeSpan EyesInterval = TimeSpan.FromSeconds(1.0 / 45.0); private static readonly bool IncludeHands = true; - private static readonly TimeSpan HandsInterval = TimeSpan.FromMilliseconds(20); + private static readonly TimeSpan HandsInterval = TimeSpan.FromSeconds(1.0 / 45.0); private static readonly bool IncludeAudio = true; @@ -101,7 +108,7 @@ public class HoloLensCaptureApp }; private static readonly Rectangle3D FrameRectangle = new ( - new Point3D(FrameDistance, 0, 0), + new (FrameDistance, 0, 0), UnitVector3D.YAxis.Negate(), UnitVector3D.ZAxis, -(FrameWidth / 2), @@ -109,7 +116,7 @@ public class HoloLensCaptureApp FrameWidth, FrameHeight - FrameBottomClip); - private static readonly Vec2 LabelSize = new Vec2(FrameWidth - FrameLabelInset * 2f, 0.008f); + private static readonly Vec2 LabelSize = new (FrameWidth - FrameLabelInset * 2f, 0.008f); private static string captureServerAddress = "0.0.0.0"; @@ -134,7 +141,7 @@ private enum GrayImageEncode Gzip, } - private static async Task Main(string[] args) + private static void Main() { // Initialize StereoKit if (!SK.Initialize( @@ -148,7 +155,7 @@ private static async Task Main(string[] args) } // Initialize MixedReality statics - await MixedReality.InitializeAsync(regenerateDefaultWorldSpatialAnchorIfNeeded: true); + MixedReality.Initialize(regenerateDefaultWorldSpatialAnchorIfNeeded: true); // Attempt to get server address from config file var docs = KnownFolders.DocumentsLibrary; @@ -167,9 +174,9 @@ private static async Task Main(string[] args) var rightFrontCamera = default(VisibleLightCamera); var leftLeftCamera = default(VisibleLightCamera); var rightRightCamera = default(VisibleLightCamera); - var head = default(HeadSensor); - var eyes = default(EyesSensor); - var hands = default(HandsSensor); + var head = default(StereoKitHeadSensor); + var eyes = default(WinRTGazeSensor); + var hands = default(OpenXRHandsSensor); IProducer audio = null; string errorMessage = null; @@ -244,9 +251,15 @@ private static async Task Main(string[] args) magnetometer = IncludeImu ? new Magnetometer(pipeline) : null; // HEAD, EYES, AND HANDS - head = IncludeHead ? new HeadSensor(pipeline, HeadInterval) : null; - eyes = IncludeEyes ? new EyesSensor(pipeline, EyesInterval) : null; - hands = IncludeHands ? new HandsSensor(pipeline, HandsInterval) : null; + head = IncludeHead ? new StereoKitHeadSensor(pipeline, HeadInterval) : null; + eyes = IncludeEyes ? new WinRTGazeSensor(pipeline, new GazeSensorConfiguration() + { + OutputEyeGaze = true, + OutputHeadGaze = false, + Interval = EyesInterval, + }) : null; + + hands = IncludeHands ? new OpenXRHandsSensor(pipeline, HandsInterval) : null; // AUDIO audio = IncludeAudio ? new Microphone(pipeline).Reframe(16384, DeliveryPolicy.Unlimited) : null; @@ -509,12 +522,12 @@ void Write(string name, IProducer producer, int port, IFormatSerializer se if (IncludeEyes) { - Write("Eyes", eyes?.Out, port++, Serializers.Ray3DFormat(), DeliveryPolicy.LatestMessage); + Write("Eyes", eyes?.Eyes, port++, Serializers.WinRTEyesFormat(), DeliveryPolicy.LatestMessage); } if (IncludeHands) { - Write("Hands", hands?.Out, port++, Serializers.HandsFormat(), DeliveryPolicy.LatestMessage); + Write("Hands", hands?.Out, port++, Serializers.OpenXRHandsFormat(), DeliveryPolicy.LatestMessage); } if (IncludeAudio) @@ -798,6 +811,11 @@ void Write(string name, IProducer producer, int port, IFormatSerializer se videoFps = 0; depthFps = 0; } + else if (StereoKitTransforms.WorldHierarchy == null) + { + frameColor = Color.Red; + labelText = "Localization temporarily lost! Re-localizing..."; + } else if (errorMessage == null) { UI.Label("Capturing ..."); diff --git a/Sources/MixedReality/HoloLensCapture/HoloLensCaptureApp/Properties/AssemblyInfo.cs b/Sources/MixedReality/HoloLensCapture/HoloLensCaptureApp/Properties/AssemblyInfo.cs index 196994143..0e329c541 100644 --- a/Sources/MixedReality/HoloLensCapture/HoloLensCaptureApp/Properties/AssemblyInfo.cs +++ b/Sources/MixedReality/HoloLensCapture/HoloLensCaptureApp/Properties/AssemblyInfo.cs @@ -12,7 +12,7 @@ [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("HoloLensCaptureApp")] -[assembly: AssemblyCopyright("© Microsoft Corporation. All rights reserved.")] +[assembly: AssemblyCopyright("© Microsoft Corporation. All rights reserved.")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -26,6 +26,6 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("0.17.52.1")] -[assembly: AssemblyFileVersion("0.17.52.1")] +[assembly: AssemblyVersion("0.18.72.1")] +[assembly: AssemblyFileVersion("0.18.72.1")] [assembly: ComVisible(false)] diff --git a/Sources/MixedReality/HoloLensCapture/HoloLensCaptureExporter/DataExporter.cs b/Sources/MixedReality/HoloLensCapture/HoloLensCaptureExporter/DataExporter.cs index 6d2188a63..a809a553b 100644 --- a/Sources/MixedReality/HoloLensCapture/HoloLensCaptureExporter/DataExporter.cs +++ b/Sources/MixedReality/HoloLensCapture/HoloLensCaptureExporter/DataExporter.cs @@ -15,6 +15,9 @@ namespace HoloLensCaptureExporter using Microsoft.Psi.Media; using Microsoft.Psi.MixedReality; using Microsoft.Psi.Spatial.Euclidean; + using OpenXRHand = Microsoft.Psi.MixedReality.OpenXR.Hand; + using StereoKitHand = Microsoft.Psi.MixedReality.StereoKit.Hand; + using WinRTEyes = Microsoft.Psi.MixedReality.WinRT.Eyes; /// /// Implements the data exporter. @@ -47,8 +50,6 @@ public static int Run(Verbs.ExportCommand exportCommand) var gyroscope = store.OpenStreamOrDefault<(Vector3D, DateTime)[]>("Gyroscope"); var magnetometer = store.OpenStreamOrDefault<(Vector3D, DateTime)[]>("Magnetometer"); var head = store.OpenStreamOrDefault("Head"); - var eyes = store.OpenStreamOrDefault("Eyes"); - var hands = store.OpenStreamOrDefault<(Hand Left, Hand Right)>("Hands"); var audio = store.OpenStreamOrDefault(AudioStreamName); var videoEncodedImageCameraView = store.OpenStreamOrDefault(VideoEncodedStreamName); var videoImageCameraView = store.OpenStreamOrDefault(VideoStreamName); @@ -184,63 +185,60 @@ void VerifyMutualExclusivity(dynamic s0, dynamic s1) // Export head, eyes and hands streams head?.Export("Head", exportCommand.OutputPath, streamWritersToClose); - eyes?.Export("Eyes", exportCommand.OutputPath, streamWritersToClose); - hands?.Select(x => x.Left).Export("Hands", "Left", exportCommand.OutputPath, streamWritersToClose); - hands?.Select(x => x.Right).Export("Hands", "Right", exportCommand.OutputPath, streamWritersToClose); - // Export audio - audio?.Export("Audio", exportCommand.OutputPath, streamWritersToClose); - - // Export scene understanding - sceneUnderstanding?.Export("SceneUnderstanding", exportCommand.OutputPath, streamWritersToClose); - - // If we have any video frames, we will export to MPEG in a new pipeline later - long videoFrameCount = 0; - var videoFrameTimeSpan = default(TimeSpan); - IStreamMetadata videoMeta = null; - if (decodedVideo is not null) + if (store.Contains("Eyes")) { - videoMeta = store.Contains(VideoStreamName) ? store.GetMetadata(VideoStreamName) : store.GetMetadata(VideoEncodedStreamName); - videoFrameCount = videoMeta.MessageCount; - videoFrameTimeSpan = videoMeta.LastMessageOriginatingTime - videoMeta.FirstMessageOriginatingTime; + var simplifiedEyesTypeName = SimplifyTypeName(store.GetMetadata("Eyes").TypeName); + if (simplifiedEyesTypeName == SimplifyTypeName(typeof(Ray3D).FullName)) + { + var eyes = store.OpenStreamOrDefault("Eyes"); + eyes?.Export("Eyes", exportCommand.OutputPath, streamWritersToClose); + } + else if (simplifiedEyesTypeName == SimplifyTypeName(typeof(WinRTEyes).FullName) || + simplifiedEyesTypeName == "Microsoft.Psi.MixedReality.EyesRT") + { + var eyes = store.OpenStreamOrDefault("Eyes"); + eyes?.Export("Eyes", exportCommand.OutputPath, streamWritersToClose); + } } - Console.WriteLine($"Exporting {exportCommand.StoreName} to {exportCommand.OutputPath}"); - p.RunAsync(ReplayDescriptor.ReplayAll, progress: new Progress(p => Console.Write($"Progress: {p:P}\r"))); - p.WaitAll(); - - foreach (var sw in streamWritersToClose) + if (store.Contains("Hands")) { - sw.Close(); + var simplifiedHandsTypeName = SimplifyTypeName(store.GetMetadata("Hands").TypeName); + if (simplifiedHandsTypeName == SimplifyTypeName(typeof((StereoKitHand, StereoKitHand)).FullName) || + simplifiedHandsTypeName == "System.ValueTuple`2[[Microsoft.Psi.MixedReality.Hand][Microsoft.Psi.MixedReality.Hand]]") + { + var hands = store.OpenStream<(StereoKitHand Left, StereoKitHand Right)>("Hands"); + hands.Select(x => x.Left).Export("Hands", "Left", exportCommand.OutputPath, streamWritersToClose); + hands.Select(x => x.Right).Export("Hands", "Right", exportCommand.OutputPath, streamWritersToClose); + } + else if (simplifiedHandsTypeName == SimplifyTypeName(typeof((OpenXRHand, OpenXRHand)).FullName) || + simplifiedHandsTypeName == "System.ValueTuple`2[[Microsoft.Psi.MixedReality.HandXR][Microsoft.Psi.MixedReality.HandXR]]") + { + var hands = store.OpenStream<(OpenXRHand Left, OpenXRHand Right)>("Hands"); + hands.Select(x => x.Left).Export("Hands", "Left", exportCommand.OutputPath, streamWritersToClose); + hands.Select(x => x.Right).Export("Hands", "Right", exportCommand.OutputPath, streamWritersToClose); + } } - Console.WriteLine(); - Console.WriteLine("Done."); - - // Export MPEG video - if (videoFrameCount > 0) - { - // Create a new pipeline - using var mpegPipeline = Pipeline.Create(deliveryPolicy: DeliveryPolicy.Throttle); - - // Open the psi store for reading - store = PsiStore.Open(mpegPipeline, exportCommand.StoreName, exportCommand.StorePath); + // Export audio + audio?.Export("Audio", exportCommand.OutputPath, streamWritersToClose); - // Get info needed for the mpeg writer - (var width, var height, var mpegStartTime, var audioFormat) = GetAudioAndVideoInfo(exportCommand.StoreName, exportCommand.StorePath); + // Export scene understanding + sceneUnderstanding?.Export("SceneUnderstanding", exportCommand.OutputPath, streamWritersToClose); - // Write the "start time" of the mpeg to file (the minimum time of the first video message and first audio message) - var mpegTimingFile = File.CreateText(EnsurePathExists(Path.Combine(exportCommand.OutputPath, "Video", "VideoMpegStartTime.txt"))); - mpegTimingFile.WriteLine($"{mpegStartTime.ToText()}"); - mpegTimingFile.Close(); + // Export MPEG video + (var videoMeta, var width, var height, var audioFormat) = GetAudioAndVideoInfo(exportCommand.StoreName, exportCommand.StorePath); + if (videoMeta is not null) + { // Configure and initialize the mpeg writer - var frameRateNumerator = (uint)(videoFrameCount - 1); - var frameRateDenominator = (uint)(videoFrameTimeSpan.TotalSeconds + 0.5); + var frameRateNumerator = (uint)(videoMeta.MessageCount - 1); + var frameRateDenominator = (uint)((videoMeta.LastMessageOriginatingTime - videoMeta.FirstMessageOriginatingTime).TotalSeconds + 0.5); var frameRate = frameRateNumerator / frameRateDenominator; var mpegFile = EnsurePathExists(Path.Combine(exportCommand.OutputPath, "Video", $"Video.mpeg")); var audioOutputFormat = WaveFormat.Create16BitPcm((int)(audioFormat?.SamplesPerSec ?? 0), audioFormat?.Channels ?? 0); - var mpegWriter = new Mpeg4Writer(mpegPipeline, mpegFile, new Mpeg4WriterConfiguration() + var mpegWriter = new Mpeg4Writer(p, mpegFile, new Mpeg4WriterConfiguration() { ImageWidth = (uint)width, ImageHeight = (uint)height, @@ -254,50 +252,85 @@ void VerifyMutualExclusivity(dynamic s0, dynamic s1) AudioSamplesPerSecond = audioOutputFormat.SamplesPerSec, }); - // Audio - store.OpenStreamOrDefault(AudioStreamName) - ?.Resample(new AudioResamplerConfiguration() { OutputFormat = audioOutputFormat, }) - ?.PipeTo(mpegWriter.AudioIn); + // We will need to resample the video stream for the mpeg + var mpegVideoInterval = TimeSpan.FromSeconds(1.0 / frameRate); + IProducer mpegVideoTicks; + IProducer mpegTicks; - // Video - decodedVideo = store.OpenStreamOrDefault(VideoStreamName) ?? - store.OpenStreamOrDefault(VideoEncodedStreamName).Decode(decoder); - - // interpolate with a frame clock for a consistent frame rate into the Mpeg4Writer - var audioMeta = store.GetMetadata(AudioStreamName); - var firstMediaOriginatingTime = (videoMeta.FirstMessageOriginatingTime < audioMeta.FirstMessageOriginatingTime ? videoMeta : audioMeta).FirstMessageOriginatingTime; - var lastMediaOriginatingTime = (videoMeta.LastMessageOriginatingTime > audioMeta.LastMessageOriginatingTime ? videoMeta : audioMeta).LastMessageOriginatingTime; - var frameClockInterval = TimeSpan.FromSeconds(1.0 / frameRate); - var frameClockTime = firstMediaOriginatingTime; - IEnumerable<(int, DateTime)> FrameClockTicks() + // Write "start" and "end" times of the mpeg to file + var mpegTimingFile = File.CreateText(EnsurePathExists(Path.Combine(exportCommand.OutputPath, "Video", "VideoMpegTiming.txt"))); + streamWritersToClose.Add(mpegTimingFile); + + // Audio + if (audioFormat is not null) + { + var mpegAudio = audio.Resample(new AudioResamplerConfiguration() { OutputFormat = audioOutputFormat, }); + mpegAudio.PipeTo(mpegWriter.AudioIn); + + // Compute frame ticks for the resampled video + mpegVideoTicks = mpegAudio + .Select((m, e) => e.OriginatingTime - m.Duration) + .Zip(decodedVideo.TimeOf()) + .Process((m, e, emitter) => + { + if (emitter.LastEnvelope.OriginatingTime == default) + { + // The mpeg "start time" will be the minimum time of the first video message and *start* of the first (resampled) audio buffer. + emitter.Post(true, m.Min()); + } + + while (emitter.LastEnvelope.OriginatingTime <= e.OriginatingTime) + { + emitter.Post(true, emitter.LastEnvelope.OriginatingTime + mpegVideoInterval); + } + }); + + // Zip with the resampled audio times + mpegTicks = mpegAudio.Select(_ => true).Zip(mpegVideoTicks).Select(a => a.First()); + } + else { - while (frameClockTime < lastMediaOriginatingTime) + // Compute frame ticks for the resampled video + mpegVideoTicks = decodedVideo.Process((_, e, emitter) => { - yield return (0, frameClockTime); - frameClockTime += frameClockInterval; - } + if (emitter.LastEnvelope.OriginatingTime == default) + { + emitter.Post(true, e.OriginatingTime); + } + + while (emitter.LastEnvelope.OriginatingTime <= e.OriginatingTime) + { + emitter.Post(true, emitter.LastEnvelope.OriginatingTime + mpegVideoInterval); + } + }); + + // No audio, so the mpeg start/end times are just the time of the first/last video message. + mpegTicks = mpegVideoTicks; } - var frameClock = Generators.Sequence(mpegPipeline, FrameClockTicks()); - var interpolatedVideo = frameClock.Join(decodedVideo, Reproducible.Nearest()); - - // The mpeg writer component processes input video frames and audio buffers, and writes to the output mpeg file. - // Because of the way the component works internally, if both input streams use a delivery policy of Throttle, - // the component ends up being much slower than if enough data is always queued at its inputs ready to be processed. - // Since the video arrives at a slower rate than the audio at the inputs to the component, we relax the - // throttling threshold at the video input such that as many video frames as possible (up to a maximum of 1000) - // are available to be processed by the mpeg writer. This avoids constantly throttling and unthrottling the - // video source, which was causing a significant slowdown in the mpeg file export. - interpolatedVideo.Select(x => x.Item2.ViewedObject).PipeTo(mpegWriter, DeliveryPolicy.QueueSizeThrottled(1000)); - - // Execute the pipeline - Console.WriteLine("Exporting MPEG video"); - mpegPipeline.RunAsync(ReplayDescriptor.ReplayAll, progress: new Progress(p => Console.Write($"Progress: {p:P}\r"))); - mpegPipeline.WaitAll(); - Console.WriteLine(); - Console.WriteLine("Done."); + // Video + mpegVideoTicks + .Join(decodedVideo, Reproducible.Nearest()).Select(tuple => tuple.Item2.ViewedObject) + .PipeTo(mpegWriter); + + // Write the mpeg start and end times time + mpegTicks.First().Do((_, e) => mpegTimingFile.WriteLine($"{e.OriginatingTime.ToText()}")); + mpegTicks.Last().Do((_, e) => mpegTimingFile.WriteLine($"{e.OriginatingTime.ToText()}")); + } + + Console.WriteLine($"Exporting {exportCommand.StoreName} to {exportCommand.OutputPath}"); + var startTime = DateTime.Now; + p.RunAsync(ReplayDescriptor.ReplayAll, progress: new Progress(p => Console.Write($"Progress: {p:P} Time elapsed: {DateTime.Now - startTime}\r"))); + p.WaitAll(); + + foreach (var sw in streamWritersToClose) + { + sw.Close(); } + Console.WriteLine(); + Console.WriteLine($"Done in {DateTime.Now - startTime}."); + return 0; } @@ -317,64 +350,66 @@ internal static string EnsurePathExists(string path) return path; } - private static (int Width, int Height, DateTime StartTime, WaveFormat audioFormat) GetAudioAndVideoInfo(string storeName, string storePath) + private static (IStreamMetadata VideoMetadata, int Width, int Height, WaveFormat AudioFormat) GetAudioAndVideoInfo(string storeName, string storePath) { // determine properties for the mpeg writer by peeking at the first video and audio messages using var p = Pipeline.Create(); var store = PsiStore.Open(p, storeName, storePath); // Get the image width and height by looking at the first message of the video stream. - // Also record the time of that first message. var width = 0; var height = 0; - var videoStartTime = default(DateTime); var videoWaitHandle = new EventWaitHandle(false, EventResetMode.ManualReset); - if (store.Contains(VideoStreamName)) - { - store.OpenStream(VideoStreamName).First().Do((v, env) => - { - (width, height) = GetWidthAndHeight(v.ViewedObject.Resource); - videoStartTime = env.OriginatingTime; - videoWaitHandle.Set(); - }); - } - else + void SetWidthAndHeight(IImage image) { - store.OpenStream(VideoEncodedStreamName).First().Do((v, env) => + if (image.PixelFormat != PixelFormat.BGRA_32bpp) { - (width, height) = GetWidthAndHeight(v.ViewedObject.Resource); - videoStartTime = env.OriginatingTime; - videoWaitHandle.Set(); - }); - } + throw new ArgumentException($"Expected video stream of {PixelFormat.BGRA_32bpp} (found {image.PixelFormat})."); + } - // Get the audio format by examining the first audio message (if one exists). - // Also record the time of that first message. - WaveFormat audioFormat = null; - var audioStartTime = default(DateTime); - var audioWaitHandle = new EventWaitHandle(false, EventResetMode.ManualReset); + width = image.Width; + height = image.Height; + videoWaitHandle.Set(); + } bool TryGetMetadata(string stream, out IStreamMetadata meta) { if (store.Contains(stream)) { meta = store.GetMetadata(stream); - return true; - } - else - { - meta = null; - return false; + if (meta.MessageCount > 0) + { + return true; + } } + + meta = null; + return false; } - if (TryGetMetadata(AudioStreamName, out var audioMeta) && audioMeta.MessageCount > 0) + if (TryGetMetadata(VideoStreamName, out var videoMetadata)) + { + store.OpenStream(VideoStreamName).First().Do(v => SetWidthAndHeight(v.ViewedObject.Resource)); + } + else if (TryGetMetadata(VideoEncodedStreamName, out videoMetadata)) + { + store.OpenStream(VideoEncodedStreamName).First().Do(v => SetWidthAndHeight(v.ViewedObject.Resource)); + } + else + { + videoWaitHandle.Set(); + } + + // Get the audio format by examining the first audio message (if one exists). + WaveFormat audioFormat = null; + var audioWaitHandle = new EventWaitHandle(false, EventResetMode.ManualReset); + + if (TryGetMetadata(AudioStreamName, out var audioMeta)) { store.OpenStream(AudioStreamName).First().Do((a, env) => { audioFormat = a.Format; - audioStartTime = env.OriginatingTime; audioWaitHandle.Set(); }); } @@ -387,24 +422,65 @@ bool TryGetMetadata(string stream, out IStreamMetadata meta) p.RunAsync(); WaitHandle.WaitAll(new WaitHandle[2] { videoWaitHandle, audioWaitHandle }); - // Determine the earlier of the two start times - var startTime = videoStartTime; - if (audioStartTime != default && audioStartTime < videoStartTime) + return (videoMetadata, width, height, audioFormat); + } + + /// + /// Simplify the full type name into just the basic underlying type names, + /// stripping away details like assembly, version, culture, token, etc. + /// For example, for the type (Vector3D, DateTime)[] + /// Input: "System.ValueTuple`2 + /// [[MathNet.Spatial.Euclidean.Vector3D, MathNet.Spatial, Version=0.6.0.0, Culture=neutral, PublicKeyToken=000000000000], + /// [System.DateTime, System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=000000000000]] + /// [], System.Private.CoreLib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=000000000000" + /// Output: "System.ValueTuple`2[[MathNet.Spatial.Euclidean.Vector3D],[System.DateTime]][]". + /// + private static string SimplifyTypeName(string typeName) + { + static string SubstringToComma(string s) { - startTime = audioStartTime; + var commaIndex = s.IndexOf(','); + if (commaIndex >= 0) + { + return s.Substring(0, commaIndex); + } + else + { + return s; + } } - return (width, height, startTime, audioFormat); - } + // Split first on open bracket, then on closed bracket + var allSplits = new List(); + foreach (var openSplit in typeName.Split('[')) + { + allSplits.Add(openSplit.Split(']')); + } - private static (int Width, int Height) GetWidthAndHeight(IImage image) - { - if (image.PixelFormat != PixelFormat.BGRA_32bpp) + // Re-assemble into a simplified string (without assembly, version, culture, token, etc). + var assembledString = string.Empty; + for (int i = 0; i < allSplits.Count; i++) { - throw new ArgumentException($"Expected video stream of {PixelFormat.BGRA_32bpp} (found {image.PixelFormat})."); + // Add back an open bracket (except the first time) + if (i != 0) + { + assembledString += "["; + } + + for (int j = 0; j < allSplits[i].Length; j++) + { + // Remove everything after the comma (assembly, version, culture, token, etc). + assembledString += SubstringToComma(allSplits[i][j]); + + // Add back a closed bracket (except the last time) + if (j != allSplits[i].Length - 1) + { + assembledString += "]"; + } + } } - return (image.Width, image.Height); + return assembledString; } } } diff --git a/Sources/MixedReality/HoloLensCapture/HoloLensCaptureExporter/Operators.cs b/Sources/MixedReality/HoloLensCapture/HoloLensCaptureExporter/Operators.cs index 228fbd6e9..017180546 100644 --- a/Sources/MixedReality/HoloLensCapture/HoloLensCaptureExporter/Operators.cs +++ b/Sources/MixedReality/HoloLensCapture/HoloLensCaptureExporter/Operators.cs @@ -17,13 +17,22 @@ namespace HoloLensCaptureExporter using Microsoft.Psi.Data; using Microsoft.Psi.Imaging; using Microsoft.Psi.MixedReality; + using Microsoft.Psi.MixedReality.OpenXR; + using Microsoft.Psi.MixedReality.WinRT; using Microsoft.Psi.Spatial.Euclidean; + using OpenXRHand = Microsoft.Psi.MixedReality.OpenXR.Hand; + using OpenXRHandsSensor = Microsoft.Psi.MixedReality.OpenXR.HandsSensor; + using StereoKitHand = Microsoft.Psi.MixedReality.StereoKit.Hand; + using StereoKitHandsSensor = Microsoft.Psi.MixedReality.StereoKit.HandsSensor; + using WinRTEyes = Microsoft.Psi.MixedReality.WinRT.Eyes; /// /// Stream operators and extension methods for exporting data. /// internal static class Operators { + private static readonly string NaNString = double.NaN.ToText(); + /// /// Opens the specified stream for reading and (or returns null if nonexistent). /// @@ -86,10 +95,15 @@ internal static string ToText(this bool b) /// to be converted. /// Tab-delimited text representation. internal static string ToText(this CoordinateSystem c) - => $"{c[0, 0].ToText()}\t{c[0, 1].ToText()}\t{c[0, 2].ToText()}\t{c[0, 3].ToText()}\t" + - $"{c[1, 0].ToText()}\t{c[1, 1].ToText()}\t{c[1, 2].ToText()}\t{c[1, 3].ToText()}\t" + - $"{c[2, 0].ToText()}\t{c[2, 1].ToText()}\t{c[2, 2].ToText()}\t{c[2, 3].ToText()}\t" + - $"{c[3, 0].ToText()}\t{c[3, 1].ToText()}\t{c[3, 2].ToText()}\t{c[3, 3].ToText()}"; + => c is not null + ? $"{c[0, 0].ToText()}\t{c[0, 1].ToText()}\t{c[0, 2].ToText()}\t{c[0, 3].ToText()}\t" + + $"{c[1, 0].ToText()}\t{c[1, 1].ToText()}\t{c[1, 2].ToText()}\t{c[1, 3].ToText()}\t" + + $"{c[2, 0].ToText()}\t{c[2, 1].ToText()}\t{c[2, 2].ToText()}\t{c[2, 3].ToText()}\t" + + $"{c[3, 0].ToText()}\t{c[3, 1].ToText()}\t{c[3, 2].ToText()}\t{c[3, 3].ToText()}" + : $"{NaNString}\t{NaNString}\t{NaNString}\t{NaNString}\t" + + $"{NaNString}\t{NaNString}\t{NaNString}\t{NaNString}\t" + + $"{NaNString}\t{NaNString}\t{NaNString}\t{NaNString}\t" + + $"{NaNString}\t{NaNString}\t{NaNString}\t{NaNString}"; /// /// Converts a camera intrinsics to a tab-delimited text representation. @@ -330,14 +344,57 @@ internal static void Export(this IProducer source, string name, string ou } /// - /// Exports a stream of hand infomation. + /// Exports a stream of EyeRT. + /// + /// The source stream of EyeRT. + /// The name for the source stream. + /// The output path. + /// The collection of stream writers to be closed. + internal static void Export(this IProducer source, string name, string outputPath, List streamWritersToClose) + { + var filePath = DataExporter.EnsurePathExists(Path.Combine(outputPath, name, $"{name}.txt")); + var file = File.CreateText(filePath); + streamWritersToClose.Add(file); + source + .Do( + (eyes, envelope) => + { + file.Write($"{envelope.OriginatingTime.ToText()}\t"); + + if (eyes?.GazeRay is null) + { + // write 6 NaNs to represent the null GazeRay (Point3D, Vector3D) + for (int i = 0; i < 6; i++) + { + file.Write($"{NaNString}\t"); + } + } + else + { + file.Write($"{eyes.GazeRay.Value.ToText()}\t"); + } + + if (eyes is null) + { + // write false for CalibrationValid + file.WriteLine(false.ToText()); + } + else + { + file.WriteLine($"{eyes.CalibrationValid.ToText()}"); + } + }); + } + + /// + /// Exports a stream of hand infomation (from ). /// /// The source stream of hand information. /// The directory in which to persist. /// The name for the source stream. /// The output path. /// The collection of stream writers to be closed. - internal static void Export(this IProducer source, string directory, string name, string outputPath, List streamWritersToClose) + internal static void Export(this IProducer source, string directory, string name, string outputPath, List streamWritersToClose) { var filePath = DataExporter.EnsurePathExists(Path.Combine(outputPath, directory, $"{name}.txt")); var file = File.CreateText(filePath); @@ -346,17 +403,63 @@ internal static void Export(this IProducer source, string directory, strin .Do( (hand, envelope) => { + // ensures that we export null Hand instances with NaNs + hand ??= StereoKitHand.Empty; + var result = new StringBuilder(); result.Append($"{envelope.OriginatingTime.ToText()}\t"); result.Append($"{hand.IsGripped.ToText()}\t"); result.Append($"{hand.IsPinched.ToText()}\t"); result.Append($"{hand.IsTracked.ToText()}\t"); - if (hand.IsTracked) + + // hand.Joints is never null, but may contain null values + foreach (var joint in hand.Joints) { - foreach (var joint in hand.Joints) - { - result.Append($"{joint.ToText()}\t"); - } + result.Append($"{joint.ToText()}\t"); + } + + file.WriteLine(result.ToString().TrimEnd('\t')); + }); + } + + /// + /// Exports a stream of hand infomation (from ). + /// + /// The source stream of hand information. + /// The directory in which to persist. + /// The name for the source stream. + /// The output path. + /// The collection of stream writers to be closed. + internal static void Export(this IProducer source, string directory, string name, string outputPath, List streamWritersToClose) + { + var filePath = DataExporter.EnsurePathExists(Path.Combine(outputPath, directory, $"{name}.txt")); + var file = File.CreateText(filePath); + streamWritersToClose.Add(file); + source + .Do( + (hand, envelope) => + { + // ensures that we export null HandXR instances with NaNs + hand ??= OpenXRHand.Empty; + + var result = new StringBuilder(); + result.Append($"{envelope.OriginatingTime.ToText()}\t"); + result.Append($"{hand.IsActive.ToText()}\t"); + + // hand.Joints is never null, but may contain null values + foreach (var joint in hand.Joints) + { + result.Append($"{joint.ToText()}\t"); + } + + foreach (var jointValid in hand.JointsValid) + { + result.Append($"{jointValid.ToText()}\t"); + } + + foreach (var jointTracked in hand.JointsTracked) + { + result.Append($"{jointTracked.ToText()}\t"); } file.WriteLine(result.ToString().TrimEnd('\t')); @@ -458,10 +561,9 @@ void BuildPoint(Point3D point, StringBuilder sb) } else { - var nan = double.NaN.ToText(); for (var i = 0; i < 8; i++) { - sb.Append($"{nan}\t"); + sb.Append($"{NaNString}\t"); } } } diff --git a/Sources/MixedReality/HoloLensCapture/HoloLensCaptureExporter/Readme.md b/Sources/MixedReality/HoloLensCapture/HoloLensCaptureExporter/Readme.md index 90323d726..84e90bf16 100644 --- a/Sources/MixedReality/HoloLensCapture/HoloLensCaptureExporter/Readme.md +++ b/Sources/MixedReality/HoloLensCapture/HoloLensCaptureExporter/Readme.md @@ -123,16 +123,16 @@ Originating Time P[x] P[y] P[z] V[x] V[y] V[z] #### Hands -Hand pose data is persisted to two text files (`Hands/Left.txt`, `Hands/Right.txt`) containing newline delimited records, each containing the originating timestamp (see above), the `IsGripped`, `IsPinched`, and `IsTracked` flags as booleans (`0` = false, `1` = true), and the pose of each joint (if tracked). For example: +Hand pose data is persisted to two text files (`Hands/Left.txt`, `Hands/Right.txt`) containing newline delimited records, each containing the originating timestamp (see above), the `IsActive` boolean flag (`0` = false, `1` = true), the pose of each joint, the valid state of each joint, and the tracked state for each joint. For example: ```text -Originating Time Gripped Pinched Tracked M₀₀ M₀₁ M₀₂ M₀₃ M₁₀ M₁₁ M₁₂ M₁₃ M₂₀ M₂₁ M₂₂ M₂₃ M₃₀ M₃₁ M₃₂ M₃₃ M₀₀ M₀₁ M₀₂ ... ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -637835897125443909 0 1 1 -0.214... -0.336... 0.916... 0.201... -0.531... -0.747... -0.399... 0.110... 0.819... -0.573... -0.018... -0.460... 0 0 0 1 0.916... -0.336... 0.214... ... -637835897125447654 0 0 0 +Originating Time Active M₀₀ M₀₁ M₀₂ M₀₃ M₁₀ M₁₁ M₁₂ M₁₃ M₂₀ M₂₁ M₂₂ M₂₃ M₃₀ M₃₁ M₃₂ M₃₃ M₀₀ M₀₁ M₀₂ ... V₁ V₂ ... V₂₆ T₁ T₂ ... T₂₆ +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ +637835897125443909 1 -0.214... -0.336... 0.916... 0.201... -0.531... -0.747... -0.399... 0.110... 0.819... -0.573... -0.018... -0.460... 0 0 0 1 0.916... -0.336... 0.214... ... 1 1 ... 1 1 1 ... 1 +637835897125447654 0 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... 0 0 ... 0 0 0 ... 0 ``` -The first three values are the flags indicating gripped/pinched/tracked state. If the hand is tracked, then the remaining are sets of 16 doubles for each of the following 26 joints (416 values). +The first value is the flag indicating active state. This is followed by sets of 16 doubles for each of the following 26 joints (416 values), followed by 26 boolean values (`0` or `1`) for the valid state of each joint (Vⱼ), followed by 26 boolean values (`0` or `1`) for the tracked state of each joint (Tⱼ). Note that missing/invalid joint values may be represented by `NaN` as in the second record above. - Palm - Wrist @@ -165,7 +165,7 @@ The first three values are the flags indicating gripped/pinched/tracked state. I Audio buffers are persisted to a WAVE file (`Audio/Audio.wav`) containing IEEE float encoded, 48KHz, single channel data. -Additionally, a set of audio buffer files in the form (`Audio000123.bin`) are persisted to the `Audio/Buffers` directory. Per-buffer originating timestamps are persisted to a `Timing.txt` file as a tab-separated pair of frame number and timestamp. For example: +Additionally, a set of audio buffer files in the form (`Audio000123.bin`) are persisted to the `Audio/Buffers` directory. Per-buffer originating timestamps are persisted to a `Timing.txt` file as a tab-separated pair of frame number and timestamp. The originating times listed within `Timing.txt` represent the *ending* time of each buffer. For example: ```text Frame Originating Time @@ -257,7 +257,7 @@ W H P₀ P₁ ... Pₙ Image frames (just for the front-facing color camera) are also exported to a single `Video.mpeg` video file in the `Video/` folder. Audio is also included in the video if available. -The start time of the video is recorded in `VideoMpegStartTime.txt`, corresponding to the earliest of the first video frame and first audio frame originating timestamps. +The start and end times of the MPEG video are recorded in `VideoMpegTiming.txt`, containing two lines corresponding to the starting ticks (earliest of the first video frame and start of the first audio buffer originating timestamps) and the ending ticks (latest of the last video frame and end of the last audio buffer). Note that the audio begins at the *start* of the first buffer and ends at the *end* of the last buffer. Also note that audio is resampled from 32-bit IEEE Float format to 16-bit PCM and so audio buffers written to the MPEG have different sizes (1920 bytes vs. 16384 bytes) compared to those of the source audio stream and the timings listed in `VideoMpegTiming.txt` may not exactly match with information in the `Audio\Buffers\Timing.txt` file. #### Scene Understanding diff --git a/Sources/MixedReality/HoloLensCapture/HoloLensCaptureInterop/Serializers.cs b/Sources/MixedReality/HoloLensCapture/HoloLensCaptureInterop/Serializers.cs index 4d9620eb7..09d7bb74c 100644 --- a/Sources/MixedReality/HoloLensCapture/HoloLensCaptureInterop/Serializers.cs +++ b/Sources/MixedReality/HoloLensCapture/HoloLensCaptureInterop/Serializers.cs @@ -10,6 +10,7 @@ namespace HoloLensCaptureInterop using System.Linq; using MathNet.Numerics.LinearAlgebra; using MathNet.Spatial.Euclidean; + using MathNet.Spatial.Units; using Microsoft.Psi; using Microsoft.Psi.Audio; using Microsoft.Psi.Calibration; @@ -20,6 +21,9 @@ namespace HoloLensCaptureInterop using Microsoft.Psi.Spatial.Euclidean; using static Microsoft.Psi.Diagnostics.PipelineDiagnostics; using Image = Microsoft.Psi.Imaging.Image; + using OpenXRHand = Microsoft.Psi.MixedReality.OpenXR.Hand; + using StereoKitHand = Microsoft.Psi.MixedReality.StereoKit.Hand; + using WinRTEyes = Microsoft.Psi.MixedReality.WinRT.Eyes; /// /// Provides serializers and deserializers for the various mixed reality streams. @@ -196,6 +200,108 @@ public static CoordinateSystem ReadCoordinateSystem(BinaryReader reader) })); } + /// + /// Write to . + /// + /// to write. + /// to which to write. + public static void WriteCoordinateSystemVelocity3D(CoordinateSystemVelocity3D coordinateSystemVelocity3D, BinaryWriter writer) + { + WriteAngularVelocity3D(coordinateSystemVelocity3D.Angular, writer); + WriteLinearVelocity3D(coordinateSystemVelocity3D.Linear, writer); + } + + /// + /// Read from . + /// + /// from which to read. + /// . + public static CoordinateSystemVelocity3D ReadCoordinateSystemVelocity3D(BinaryReader reader) + { + var angularVelocity3D = ReadAngularVelocity3D(reader); + var linearVelocity3D = ReadLinearVelocity3D(reader); + return new CoordinateSystemVelocity3D(angularVelocity3D, linearVelocity3D); + } + + /// + /// Write to . + /// + /// to write. + /// to which to write. + public static void WriteAngularVelocity3D(AngularVelocity3D angularVelocity3D, BinaryWriter writer) + { + // Write the origin rotation + WriteBool(angularVelocity3D.OriginRotation != null, writer); + if (angularVelocity3D.OriginRotation != null) + { + var or = angularVelocity3D.OriginRotation.AsColumnMajorArray(); + for (var i = 0; i < 9; i++) + { + writer.Write(or[i]); + } + } + + WriteUnitVector3D(angularVelocity3D.AxisDirection, writer); + WriteAngle(angularVelocity3D.Magnitude, writer); + } + + /// + /// Read from . + /// + /// from which to read. + /// . + public static AngularVelocity3D ReadAngularVelocity3D(BinaryReader reader) + { + Matrix originRotation = default; + var hasOriginRotation = ReadBool(reader); + if (hasOriginRotation) + { + var or = new double[9]; + for (var i = 0; i < 9; i++) + { + or[i] = reader.ReadDouble(); + } + + originRotation = + Matrix.Build.DenseOfArray(new double[,] + { + { or[0], or[3], or[6] }, + { or[1], or[4], or[7] }, + { or[2], or[5], or[8] }, + }); + } + + var axisDirection = ReadUnitVector3D(reader); + var magnitude = ReadAngle(reader); + return hasOriginRotation ? new AngularVelocity3D(originRotation, axisDirection, magnitude) : default; + } + + /// + /// Write to . + /// + /// to write. + /// to which to write. + public static void WriteLinearVelocity3D(LinearVelocity3D linearVelocity3D, BinaryWriter writer) + { + // Write the origin rotation + WritePoint3D(linearVelocity3D.Origin, writer); + WriteUnitVector3D(linearVelocity3D.Direction, writer); + writer.Write(linearVelocity3D.Magnitude); + } + + /// + /// Read from . + /// + /// from which to read. + /// . + public static LinearVelocity3D ReadLinearVelocity3D(BinaryReader reader) + { + var origin = ReadPoint3D(reader); + var direction = ReadUnitVector3D(reader); + var magnitude = reader.ReadDouble(); + return new LinearVelocity3D(origin, direction, magnitude); + } + /// /// Format for . /// @@ -259,18 +365,18 @@ public static CalibrationPointsMap ReadCalibrationPointsMap(BinaryReader reader) }; /// - /// Format for . + /// Format for . /// /// serializer/deserializer. - public static Format HandFormat() - => new (WriteHand, ReadHand); + public static Format StereoKitHandFormat() + => new (WriteStereoKitHand, ReadStereoKitHand); /// - /// Write to . + /// Write to . /// - /// to write. + /// to write. /// to which to write. - public static void WriteHand(Hand hand, BinaryWriter writer) + public static void WriteStereoKitHand(StereoKitHand hand, BinaryWriter writer) { WriteBool(hand != null, writer); if (hand == null) @@ -281,19 +387,15 @@ public static void WriteHand(Hand hand, BinaryWriter writer) WriteBool(hand.IsTracked, writer); WriteBool(hand.IsPinched, writer); WriteBool(hand.IsGripped, writer); - - foreach (var j in hand.Joints) - { - WriteCoordinateSystem(j, writer); - } + WriteCollection(hand.Joints, writer, WriteCoordinateSystem); } /// - /// Read from . + /// Read from . /// /// from which to read. - /// . - public static Hand ReadHand(BinaryReader reader) + /// . + public static StereoKitHand ReadStereoKitHand(BinaryReader reader) { if (!ReadBool(reader)) { @@ -303,42 +405,150 @@ public static Hand ReadHand(BinaryReader reader) var isTracked = ReadBool(reader); var isPinched = ReadBool(reader); var isGripped = ReadBool(reader); - var numJoints = (int)HandJointIndex.MaxIndex; - var joints = new CoordinateSystem[numJoints]; - for (var i = 0; i < numJoints; i++) + var joints = ReadCollection(reader, ReadCoordinateSystem).ToArray(); + return new StereoKitHand(isTracked, isPinched, isGripped, joints); + } + + /// + /// Format for . + /// + /// of serializer/deserializer. + public static Format<(StereoKitHand Left, StereoKitHand Right)> StereoKitHandsFormat() + => new (WriteStereoKitHands, ReadStereoKitHands); + + /// + /// Write to . + /// + /// to write. + /// to which to write. + public static void WriteStereoKitHands((StereoKitHand Left, StereoKitHand Right) hands, BinaryWriter writer) + { + WriteStereoKitHand(hands.Left, writer); + WriteStereoKitHand(hands.Right, writer); + } + + /// + /// Read from . + /// + /// from which to read. + /// . + public static (StereoKitHand Left, StereoKitHand Right) ReadStereoKitHands(BinaryReader reader) + { + return (ReadStereoKitHand(reader), ReadStereoKitHand(reader)); + } + + /// + /// Format for . + /// + /// serializer/deserializer. + public static Format OpenXRHandFormat() + => new (WriteOpenXRHand, ReadOpenXRHand); + + /// + /// Write to . + /// + /// to write. + /// to which to write. + public static void WriteOpenXRHand(OpenXRHand hand, BinaryWriter writer) + { + WriteBool(hand != null, writer); + if (hand == null) { - joints[i] = ReadCoordinateSystem(reader); + return; } - return new Hand(isTracked, isPinched, isGripped, joints); + WriteBool(hand.IsActive, writer); + WriteCollection(hand.Joints, writer, WriteCoordinateSystem); + WriteCollection(hand.JointsValid, writer, WriteBool); + WriteCollection(hand.JointsTracked, writer, WriteBool); } /// - /// Format for . + /// Read from . /// - /// of serializer/deserializer. - public static Format<(Hand Left, Hand Right)> HandsFormat() - => new (WriteHands, ReadHands); + /// from which to read. + /// . + public static OpenXRHand ReadOpenXRHand(BinaryReader reader) + { + if (!ReadBool(reader)) + { + return null; + } + + var isActive = ReadBool(reader); + var joints = ReadCollection(reader, ReadCoordinateSystem).ToArray(); + var jointsValid = ReadCollection(reader, ReadBool).ToArray(); + var jointsTracked = ReadCollection(reader, ReadBool).ToArray(); + return new OpenXRHand(isActive, joints, jointsValid, jointsTracked); + } + + /// + /// Format for . + /// + /// serializer/deserializer. + public static Format WinRTEyesFormat() + => new (WriteWinRTEyes, ReadWinRTEyes); /// - /// Write to . + /// Write to . /// - /// to write. + /// to write. /// to which to write. - public static void WriteHands((Hand Left, Hand Right) hands, BinaryWriter writer) + public static void WriteWinRTEyes(WinRTEyes eyes, BinaryWriter writer) { - WriteHand(hands.Left, writer); - WriteHand(hands.Right, writer); + WriteBool(eyes != null, writer); + if (eyes == null) + { + return; + } + + WriteBool(eyes.CalibrationValid, writer); + WriteNullable(eyes.GazeRay, writer, WriteRay3D); } /// - /// Read from . + /// Read from . /// /// from which to read. - /// . - public static (Hand Left, Hand Right) ReadHands(BinaryReader reader) + /// . + public static WinRTEyes ReadWinRTEyes(BinaryReader reader) { - return (ReadHand(reader), ReadHand(reader)); + if (!ReadBool(reader)) + { + return null; + } + + var calibrationValid = ReadBool(reader); + var gazeRay = ReadNullable(reader, ReadRay3D); + return new WinRTEyes(gazeRay, calibrationValid); + } + + /// + /// Format for . + /// + /// of serializer/deserializer. + public static Format<(OpenXRHand Left, OpenXRHand Right)> OpenXRHandsFormat() + => new (WriteOpenXRHands, ReadOpenXRHands); + + /// + /// Write to . + /// + /// to write. + /// to which to write. + public static void WriteOpenXRHands((OpenXRHand Left, OpenXRHand Right) hands, BinaryWriter writer) + { + WriteOpenXRHand(hands.Left, writer); + WriteOpenXRHand(hands.Right, writer); + } + + /// + /// Read from . + /// + /// from which to read. + /// . + public static (OpenXRHand Left, OpenXRHand Right) ReadOpenXRHands(BinaryReader reader) + { + return (ReadOpenXRHand(reader), ReadOpenXRHand(reader)); } /// @@ -1171,6 +1381,58 @@ public static Vector3D ReadVector3D(BinaryReader reader) return new Vector3D(x, y, z); } + /// + /// Write to . + /// + /// to write. + /// to which to write. + public static void WriteUnitVector3D(UnitVector3D unitVector3D, BinaryWriter writer) + { + writer.Write(unitVector3D.X); + writer.Write(unitVector3D.Y); + writer.Write(unitVector3D.Z); + } + + /// + /// Read from . + /// + /// from which to read. + /// . + public static UnitVector3D ReadUnitVector3D(BinaryReader reader) + { + var x = reader.ReadDouble(); + var y = reader.ReadDouble(); + var z = reader.ReadDouble(); + if (x == 0 && y == 0 && z == 0) + { + return default; + } + else + { + return UnitVector3D.Create(x, y, z); + } + } + + /// + /// Write to . + /// + /// to write. + /// to which to write. + public static void WriteAngle(Angle angle, BinaryWriter writer) + { + writer.Write(angle.Radians); + } + + /// + /// Read from . + /// + /// from which to read. + /// . + public static Angle ReadAngle(BinaryReader reader) + { + return Angle.FromRadians(reader.ReadDouble()); + } + /// /// Format for . /// diff --git a/Sources/MixedReality/HoloLensCapture/HoloLensCaptureServer/HoloLensCaptureServer.cs b/Sources/MixedReality/HoloLensCapture/HoloLensCaptureServer/HoloLensCaptureServer.cs index eee154a68..f16e71d77 100644 --- a/Sources/MixedReality/HoloLensCapture/HoloLensCaptureServer/HoloLensCaptureServer.cs +++ b/Sources/MixedReality/HoloLensCapture/HoloLensCaptureServer/HoloLensCaptureServer.cs @@ -21,6 +21,9 @@ namespace HoloLensCaptureServer using Microsoft.Psi.Interop.Transport; using Microsoft.Psi.MixedReality; using Microsoft.Psi.Spatial.Euclidean; + using OpenXRHand = Microsoft.Psi.MixedReality.OpenXR.Hand; + using StereoKitHand = Microsoft.Psi.MixedReality.StereoKit.Hand; + using WinRTEyes = Microsoft.Psi.MixedReality.WinRT.Eyes; /// /// Capture server to persist streams from the accompanying HoloLencCaptureApp. @@ -31,75 +34,85 @@ public class HoloLensCaptureServer private const string Version = "v1"; // capture actions to execute for expected stream types - private static readonly Dictionary> CaptureStreamAction = new () + private static readonly Dictionary> CaptureStreamAction = new () { { // CoordinateSystem SimplifyTypeName(typeof(CoordinateSystem).FullName), - (s, t) => CaptureTcpStream(s, t, Serializers.CoordinateSystemFormat()) + (t) => CaptureTcpStream(t, Serializers.CoordinateSystemFormat()) }, { // Ray3D SimplifyTypeName(typeof(Ray3D).FullName), - (s, t) => CaptureTcpStream(s, t, Serializers.Ray3DFormat()) + (t) => CaptureTcpStream(t, Serializers.Ray3DFormat()) }, { - // Hand - SimplifyTypeName(typeof(Hand).FullName), - (s, t) => CaptureTcpStream(s, t, Serializers.HandFormat()) + // StereoKit Hand + SimplifyTypeName(typeof(StereoKitHand).FullName), + (t) => CaptureTcpStream(t, Serializers.StereoKitHandFormat()) + }, + { + // OpenXR Hand + SimplifyTypeName(typeof(OpenXRHand).FullName), + (t) => CaptureTcpStream(t, Serializers.OpenXRHandFormat()) + }, + { + // WinRT Eyes + SimplifyTypeName(typeof(WinRTEyes).FullName), + (t) => CaptureTcpStream(t, Serializers.WinRTEyesFormat(), persistFrameRate: true) }, { // AudioBuffer SimplifyTypeName(typeof(AudioBuffer).FullName), - (s, t) => CaptureTcpStream(s, t, Serializers.AudioBufferFormat()) + (t) => CaptureTcpStream(t, Serializers.AudioBufferFormat()) }, { // Shared SimplifyTypeName(typeof(Shared).FullName), - (s, t) => ViewImageStream(CaptureTcpStream>(s, t, Serializers.SharedImageFormat(), largeMessage: true), s.StreamName) + (t) => ViewImageStream(CaptureTcpStream>(t, Serializers.SharedImageFormat(), largeMessage: true), t.Stream.StreamName) }, { // Shared SimplifyTypeName(typeof(Shared).FullName), - (s, t) => ViewImageStream(CaptureTcpStream>(s, t, Serializers.SharedEncodedImageFormat(), largeMessage: true, persistFrameRate: true).Decode(new ImageFromStreamDecoder(), DeliveryPolicy.LatestMessage), s.StreamName) + (t) => ViewImageStream(CaptureTcpStream>(t, Serializers.SharedEncodedImageFormat(), largeMessage: true, persistFrameRate: true).Decode(new ImageFromStreamDecoder(), DeliveryPolicy.LatestMessage), t.Stream.StreamName) }, { // Shared SimplifyTypeName(typeof(Shared).FullName), - (s, t) => CaptureTcpStream>(s, t, Serializers.SharedDepthImageFormat(), largeMessage: true, persistFrameRate: true) + (t) => CaptureTcpStream>(t, Serializers.SharedDepthImageFormat(), largeMessage: true, persistFrameRate: true) }, { // CameraIntrinsics SimplifyTypeName(typeof(CameraIntrinsics).FullName), - (s, t) => CaptureTcpStream(s, t, Serializers.CameraIntrinsicsFormat()) + (t) => CaptureTcpStream(t, Serializers.CameraIntrinsicsFormat()) }, { // CalibrationPointsMap SimplifyTypeName(typeof(CalibrationPointsMap).FullName), - (s, t) => CaptureTcpStream(s, t, Serializers.CalibrationPointsMapFormat(), largeMessage: true) + (t) => CaptureTcpStream(t, Serializers.CalibrationPointsMapFormat(), largeMessage: true) }, { // SceneObjectCollection SimplifyTypeName(typeof(SceneObjectCollection).FullName), - (s, t) => CaptureTcpStream(s, t, Serializers.SceneObjectCollectionFormat(), largeMessage: true) + (t) => CaptureTcpStream(t, Serializers.SceneObjectCollectionFormat(), largeMessage: true) }, { // PipelineDiagnostics SimplifyTypeName(typeof(PipelineDiagnostics).FullName), - (s, t) => CaptureTcpStream(s, t, Serializers.PipelineDiagnosticsFormat()) + (t) => CaptureTcpStream(t, Serializers.PipelineDiagnosticsFormat()) }, { // int SimplifyTypeName(typeof(int).FullName), - (s, t) => CaptureTcpStream(s, t, Serializers.Int32Format()) + (t) => CaptureTcpStream(t, Serializers.Int32Format()) }, { // (Vector3D, DateTime)[] SimplifyTypeName(typeof((Vector3D, DateTime)[]).FullName), - (s, t) => + (t) => { // relay *frames* of IMU samples - CaptureTcpStream<(Vector3D, DateTime)[]>(s, t, Serializers.ImuFormat()); + CaptureTcpStream<(Vector3D, DateTime)[]>(t, Serializers.ImuFormat()); // relay *individual* IMU samples // GetTcpStream<(Vector3D, DateTime)[]>(Serializers.ImuFormat()).SelectManyImuSamples().Write(stream.StreamName, store); @@ -107,23 +120,28 @@ public class HoloLensCaptureServer }, { // (Hand Left, Hand Right) - SimplifyTypeName(typeof((Hand Left, Hand Right)).FullName), - (s, t) => CaptureTcpStream<(Hand Left, Hand Right)>(s, t, Serializers.HandsFormat(), persistFrameRate: true) + SimplifyTypeName(typeof((StereoKitHand Left, StereoKitHand Right)).FullName), + (t) => CaptureTcpStream<(StereoKitHand Left, StereoKitHand Right)>(t, Serializers.StereoKitHandsFormat(), persistFrameRate: true) + }, + { + // (HandXR Left, HandXR Right) + SimplifyTypeName(typeof((OpenXRHand Left, OpenXRHand Right)).FullName), + (t) => CaptureTcpStream<(OpenXRHand Left, OpenXRHand Right)>(t, Serializers.OpenXRHandsFormat(), persistFrameRate: true) }, { // EncodedImageCameraView SimplifyTypeName(typeof(EncodedImageCameraView).FullName), - (s, t) => ViewImageStream(CaptureTcpStream(s, t, Serializers.EncodedImageCameraViewFormat(), true, t => t.Dispose(), true).Select(v => v.ViewedObject).Decode(new ImageFromStreamDecoder(), DeliveryPolicy.LatestMessage), s.StreamName) + (t) => ViewImageStream(CaptureTcpStream(t, Serializers.EncodedImageCameraViewFormat(), true, t => t.Dispose(), true).Select(v => v.ViewedObject).Decode(new ImageFromStreamDecoder(), DeliveryPolicy.LatestMessage), t.Stream.StreamName) }, { // ImageCameraView SimplifyTypeName(typeof(ImageCameraView).FullName), - (s, t) => ViewImageStream(CaptureTcpStream(s, t, Serializers.ImageCameraViewFormat(), true, t => t.Dispose(), true).Select(v => v.ViewedObject), s.StreamName) + (t) => ViewImageStream(CaptureTcpStream(t, Serializers.ImageCameraViewFormat(), true, t => t.Dispose(), true).Select(v => v.ViewedObject), t.Stream.StreamName) }, { // DepthImageCameraView SimplifyTypeName(typeof(DepthImageCameraView).FullName), - (s, t) => CaptureTcpStream(s, t, Serializers.DepthImageCameraViewFormat(), true, t => t.Dispose(), true) + (t) => CaptureTcpStream(t, Serializers.DepthImageCameraViewFormat(), true, t => t.Dispose(), true) }, }; @@ -228,17 +246,6 @@ private static void CreateAndRunComputeServerPipeline(Rendezvous.Process inputRe captureServerPipeline.Diagnostics.Write("ServerDiagnostics", captureServerStore); - // Detect app disconnect - var lastAppHeartBeat = DateTime.MaxValue; - var appHeartBeatTimeout = TimeSpan.FromSeconds(20); - Timers.Timer(captureServerPipeline, TimeSpan.FromSeconds(1)).Do(_ => - { - if (DateTime.UtcNow - lastAppHeartBeat > appHeartBeatTimeout) - { - StopComputeServerPipeline("APPLICATION HEARTBEAT LOST"); - } - }); - // Connect to remote clock on the client app to synchronize clocks foreach (var endpoint in inputRendezvousProcess.Endpoints) { @@ -259,21 +266,18 @@ private static void CreateAndRunComputeServerPipeline(Rendezvous.Process inputRe statistics = new (); foreach (var endpoint in inputRendezvousProcess.Endpoints) { - if (endpoint is Rendezvous.TcpSourceEndpoint tcpEndpoint) + if (endpoint is Rendezvous.TcpSourceEndpoint tcpEndpoint && tcpEndpoint.Stream is not null) { - foreach (var stream in tcpEndpoint.Streams) - { - // Determine the correct action to execute for capturing the rendezvous stream, - // based on a simplified version of the stream's type name. - var simpleTypeName = SimplifyTypeName(stream.TypeName); - - if (!CaptureStreamAction.ContainsKey(simpleTypeName)) - { - throw new Exception($"Unknown stream type: {stream.StreamName} ({stream.TypeName})"); - } + // Determine the correct action to execute for capturing the rendezvous stream, + // based on a simplified version of the stream's type name. + var simpleTypeName = SimplifyTypeName(tcpEndpoint.Stream.TypeName); - CaptureStreamAction[simpleTypeName](stream, tcpEndpoint); + if (!CaptureStreamAction.ContainsKey(simpleTypeName)) + { + throw new Exception($"Unknown stream type: {tcpEndpoint.Stream.StreamName} ({tcpEndpoint.Stream.TypeName})"); } + + CaptureStreamAction[simpleTypeName](tcpEndpoint); } else if (endpoint is not Rendezvous.RemoteClockExporterEndpoint) { @@ -352,26 +356,26 @@ private static void CreateAndRunComputeServerPipeline(Rendezvous.Process inputRe } private static IProducer CaptureTcpStream( - Rendezvous.Stream stream, Rendezvous.TcpSourceEndpoint tcpEndpoint, IFormatDeserializer deserializer, bool largeMessage = false, Action deallocator = null, bool persistFrameRate = false) { + var streamName = tcpEndpoint.Stream.StreamName; var tcpSource = tcpEndpoint.ToTcpSource(captureServerPipeline, deserializer, deallocator: deallocator); var stats = new StreamStatistics(); - statistics.Add(stream.StreamName, stats); + statistics.Add(streamName, stats); tcpSource .Do((_, e) => stats.ReportMessage(e.OriginatingTime)) - .Write(stream.StreamName, captureServerStore, largeMessage); + .Write(streamName, captureServerStore, largeMessage); if (persistFrameRate) { tcpSource .Window(TimeSpan.FromSeconds(-3), TimeSpan.Zero, DeliveryPolicy.SynchronousOrThrottle) .Select(b => b.Length / 3.0, DeliveryPolicy.SynchronousOrThrottle) - .Write($"{stream.StreamName}.AvgFrameRate", captureServerStore); + .Write($"{streamName}.AvgFrameRate", captureServerStore); } return tcpSource; diff --git a/Sources/MixedReality/HoloLensCapture/HoloLensCaptureServer/Readme.md b/Sources/MixedReality/HoloLensCapture/HoloLensCaptureServer/Readme.md index aa4406fda..ffdef6850 100644 --- a/Sources/MixedReality/HoloLensCapture/HoloLensCaptureServer/Readme.md +++ b/Sources/MixedReality/HoloLensCapture/HoloLensCaptureServer/Readme.md @@ -40,4 +40,4 @@ The server app persists all streams to a local \\psi store. This store can be op Note that the visualizer for hand tracking data is defined in `Microsoft.Psi.MixedReality.Visualization.Windows`. The visualizers for 3D depth and image camera views are defined in `Microsoft.Psi.Spatial.Euclidean.Visualization.Windows`. Follow the instructions for [3rd Party Visualizers](https://github.com/microsoft/psi/wiki/3rd-Party-Visualizers) to add those projects' assemblies to `PsiStudioSettings.xml` in order to visualize 3D hands and camera views in PsiStudio. -You may need to double-click a stream (or right-click and select "Expand Members") in order to drill down into sub-streams that can be visualized. For example, hands may be persisted in a stream of tuples, which can be expanded to reveal derived sub-streams for the left and right hand (each of which can be visualized separately). Any of the `CameraView` streams can be similarly expanded into `CameraIntrinsics`, `CameraPose`, and `ViewedObject` members. Right-click on `ViewedObject` to reveal options for visualizing as a 2D image. +You may need to double-click a stream (or right-click and select "Add Member Derived Streams") in order to drill down into sub-streams that can be visualized. For example, hands may be persisted in a stream of tuples, which can be expanded to reveal derived sub-streams for the left and right hand (each of which can be visualized separately). Any of the `CameraView` streams can be similarly expanded into `CameraIntrinsics`, `CameraPose`, and `ViewedObject` members. Right-click on `ViewedObject` to reveal options for visualizing as a 2D image. diff --git a/Sources/MixedReality/HoloLensCapture/Readme.md b/Sources/MixedReality/HoloLensCapture/Readme.md index a29e6191d..94aca7bf3 100644 --- a/Sources/MixedReality/HoloLensCapture/Readme.md +++ b/Sources/MixedReality/HoloLensCapture/Readme.md @@ -14,4 +14,4 @@ The server app persists all streams to a local \\psi store. This store can be op Note that the visualizer for hand tracking data is defined in `Microsoft.Psi.MixedReality.Visualization.Windows`. The visualizers for 3D depth and image camera views are defined in `Microsoft.Psi.Spatial.Euclidean.Visualization.Windows`. Follow the instructions for [3rd Party Visualizers](https://github.com/microsoft/psi/wiki/3rd-Party-Visualizers) to add those projects' assemblies to `PsiStudioSettings.xml` in order to visualize 3D hands and camera views in PsiStudio. -You may need to double-click a stream (or right-click and select "Expand Members") in order to drill down into sub-streams that can be visualized. For example, hands may be persisted in a stream of tuples, which can be expanded to reveal derived sub-streams for the left and right hand (each of which can be visualized separately). Any of the `CameraView` streams can be similarly expanded into `CameraIntrinsics`, `CameraPose`, and `ViewedObject` members. Right-click on `ViewedObject` to reveal options for visualizing as a 2D image. +You may need to double-click a stream (or right-click and select "Add Member Derived Streams") in order to drill down into sub-streams that can be visualized. For example, hands may be persisted in a stream of tuples, which can be expanded to reveal derived sub-streams for the left and right hand (each of which can be visualized separately). Any of the `CameraView` streams can be similarly expanded into `CameraIntrinsics`, `CameraPose`, and `ViewedObject` members. Right-click on `ViewedObject` to reveal options for visualizing as a 2D image. diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/ImageToJpegStreamEncoder.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/ImageToJpegStreamEncoder.cs index 9b49ac091..726478fcf 100644 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/ImageToJpegStreamEncoder.cs +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/ImageToJpegStreamEncoder.cs @@ -1,12 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -namespace Microsoft.Psi.MixedReality +namespace Microsoft.Psi.Imaging { using System; using System.IO; using System.Threading.Tasks; - using Microsoft.Psi.Imaging; using Windows.Graphics.Imaging; /// @@ -24,8 +23,10 @@ public class ImageToJpegStreamEncoder : IImageToStreamEncoder public ImageToJpegStreamEncoder(double imageQuality = 1.0) { this.imageQuality = imageQuality; - this.propertySet = new (); - this.propertySet.Add("ImageQuality", new BitmapTypedValue(imageQuality, Windows.Foundation.PropertyType.Single)); + this.propertySet = new () + { + { "ImageQuality", new BitmapTypedValue(imageQuality, Windows.Foundation.PropertyType.Single) }, + }; } /// diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/MediaCaptureMicrophone.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/MediaCapture/Microphone.cs similarity index 95% rename from Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/MediaCaptureMicrophone.cs rename to Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/MediaCapture/Microphone.cs index a365b9990..f5faeb2ea 100644 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/MediaCaptureMicrophone.cs +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/MediaCapture/Microphone.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -namespace Microsoft.Psi.MixedReality +namespace Microsoft.Psi.MixedReality.MediaCapture { using System; using System.Collections.Generic; @@ -18,27 +18,21 @@ namespace Microsoft.Psi.MixedReality using AudioBuffer = Microsoft.Psi.Audio.AudioBuffer; /// - /// Microphone source component with MediaCapture. + /// Source component for a microphone. /// - public class MediaCaptureMicrophone : ISourceComponent, IProducer, IDisposable + public class Microphone : ISourceComponent, IProducer, IDisposable { - private readonly MediaCaptureMicrophoneConfiguration configuration; + private readonly MicrophoneConfiguration configuration; private readonly Pipeline pipeline; private readonly string name; private readonly Task initMediaCaptureTask; - private MediaCapture mediaCapture; - private MediaFrameReader audioFrameReader; - private TypedEventHandler audioFrameHandler; - - private WaveFormat audioFormat; - /// /// Gets the AudioEncodingProperties subtype as a WaveFormatTag. /// Please see the link below for the most recently updated list of subtypes: /// https://docs.microsoft.com/en-us/uwp/api/windows.media.mediaproperties.audioencodingproperties.subtype?view=winrt-22621. /// - private Dictionary findSubtypeAsWaveFormatTag = new () + private readonly Dictionary findSubtypeAsWaveFormatTag = new () { // Advanced Audio Coding (AAC). The stream can contain either raw AAC data or AAC data in an Audio Data Transport Stream (ADTS) stream. { "AAC", WaveFormatTag.WAVE_FORMAT_UNKNOWN }, @@ -104,17 +98,23 @@ public class MediaCaptureMicrophone : ISourceComponent, IProducer, { "Vorbis", WaveFormatTag.WAVE_FORMAT_UNKNOWN }, }; + private MediaCapture mediaCapture; + private MediaFrameReader audioFrameReader; + private TypedEventHandler audioFrameHandler; + + private WaveFormat audioFormat; + /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The pipeline to add the component to. /// The configuration for this component. /// An optional name for the component. - public MediaCaptureMicrophone(Pipeline pipeline, MediaCaptureMicrophoneConfiguration configuration = null, string name = nameof(MediaCaptureMicrophone)) + public Microphone(Pipeline pipeline, MicrophoneConfiguration configuration = null, string name = nameof(Microphone)) { this.name = name; this.pipeline = pipeline; - this.configuration = configuration ?? new MediaCaptureMicrophoneConfiguration(); + this.configuration = configuration ?? new MicrophoneConfiguration(); this.Out = pipeline.CreateEmitter(this, nameof(this.Out)); @@ -243,7 +243,7 @@ private async Task CreateMediaFrameReaderAsync(MediaStreamType /// The stream on which to post the audio buffer. /// The event handler. private TypedEventHandler CreateMediaFrameHandler( - MediaCaptureMicrophoneConfiguration audioSettings, + MicrophoneConfiguration audioSettings, Emitter audioStream) { return (sender, args) => diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/MediaCaptureMicrophoneConfiguration.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/MediaCapture/MicrophoneConfiguration.cs similarity index 86% rename from Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/MediaCaptureMicrophoneConfiguration.cs rename to Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/MediaCapture/MicrophoneConfiguration.cs index d0f74ebe6..746f27005 100644 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/MediaCaptureMicrophoneConfiguration.cs +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/MediaCapture/MicrophoneConfiguration.cs @@ -1,12 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -namespace Microsoft.Psi.MixedReality +namespace Microsoft.Psi.MixedReality.MediaCapture { /// - /// Configuration for the component. + /// Configuration for the component. /// - public class MediaCaptureMicrophoneConfiguration + public class MicrophoneConfiguration { /// /// Gets or sets a value indicating whether the audio buffer is emitted. diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/MixedRealityCapturePerspective.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/MediaCapture/MixedRealityCapturePerspective.cs similarity index 93% rename from Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/MixedRealityCapturePerspective.cs rename to Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/MediaCapture/MixedRealityCapturePerspective.cs index facd2b364..c8fac7d7a 100644 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/MixedRealityCapturePerspective.cs +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/MediaCapture/MixedRealityCapturePerspective.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -namespace Microsoft.Psi.MixedReality +namespace Microsoft.Psi.MixedReality.MediaCapture { /// /// Enumeration which indicates the perspective from which holograms are rendered in the mixed-reality image. diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/MixedRealityCaptureVideoEffect.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/MediaCapture/MixedRealityCaptureVideoEffect.cs similarity index 98% rename from Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/MixedRealityCaptureVideoEffect.cs rename to Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/MediaCapture/MixedRealityCaptureVideoEffect.cs index 8000280d7..24fbd04e9 100644 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/MixedRealityCaptureVideoEffect.cs +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/MediaCapture/MixedRealityCaptureVideoEffect.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -namespace Microsoft.Psi.MixedReality +namespace Microsoft.Psi.MixedReality.MediaCapture { using Windows.Foundation; using Windows.Foundation.Collections; diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/PhotoVideoCamera.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/MediaCapture/PhotoVideoCamera.cs similarity index 99% rename from Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/PhotoVideoCamera.cs rename to Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/MediaCapture/PhotoVideoCamera.cs index feac32fb6..95fdd1928 100644 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/PhotoVideoCamera.cs +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/MediaCapture/PhotoVideoCamera.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -namespace Microsoft.Psi.MixedReality +namespace Microsoft.Psi.MixedReality.MediaCapture { using System; using System.Collections.Generic; diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/PhotoVideoCameraConfiguration.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/MediaCapture/PhotoVideoCameraConfiguration.cs similarity index 99% rename from Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/PhotoVideoCameraConfiguration.cs rename to Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/MediaCapture/PhotoVideoCameraConfiguration.cs index e1d9fe86f..e6407ff9d 100644 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/PhotoVideoCameraConfiguration.cs +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/MediaCapture/PhotoVideoCameraConfiguration.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -namespace Microsoft.Psi.MixedReality +namespace Microsoft.Psi.MixedReality.MediaCapture { /// /// Configuration for the component. diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/UnsafeNative.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/MediaCapture/UnsafeNative.cs similarity index 95% rename from Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/UnsafeNative.cs rename to Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/MediaCapture/UnsafeNative.cs index 081697eaa..d44d5eb90 100644 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/UnsafeNative.cs +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/MediaCapture/UnsafeNative.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -namespace Microsoft.Psi.MixedReality +namespace Microsoft.Psi.MixedReality.MediaCapture { using System; using System.Runtime.InteropServices; diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/Microsoft.Psi.MixedReality.UniversalWindows.csproj b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/Microsoft.Psi.MixedReality.UniversalWindows.csproj index 4f5800c66..00a1a8347 100644 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/Microsoft.Psi.MixedReality.UniversalWindows.csproj +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/Microsoft.Psi.MixedReality.UniversalWindows.csproj @@ -54,33 +54,35 @@ PackageReference - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - + + + @@ -99,7 +101,7 @@ all - 0.3.5 + 0.3.6 1.1.118 @@ -141,6 +143,7 @@ + 14.0 diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/MixedReality.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/MixedReality.cs index 949c0c1f2..7987f786e 100644 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/MixedReality.cs +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/MixedReality.cs @@ -4,8 +4,9 @@ namespace Microsoft.Psi.MixedReality { using System; - using System.Threading.Tasks; - using StereoKit; + using global::StereoKit; + using global::StereoKit.Framework; + using Microsoft.Psi.MixedReality.StereoKit; using Windows.Perception.Spatial; /// @@ -34,19 +35,18 @@ public static class MixedReality /// /// A spatial anchor to use for the world (optional). /// Optional flag indicating whether to regenerate and persist default world spatial anchor if currently persisted anchor fails to localize in the current environment (default: false). - /// A representing the asynchronous operation. /// /// This method should be called after SK.Initialize. /// - public static async Task InitializeAsync(SpatialAnchor worldSpatialAnchor = null, bool regenerateDefaultWorldSpatialAnchorIfNeeded = false) + public static void Initialize(SpatialAnchor worldSpatialAnchor = null, bool regenerateDefaultWorldSpatialAnchorIfNeeded = false) { if (!SK.IsInitialized) { - throw new InvalidOperationException("StereoKit is not initialized. Call SK.Initialize before calling MixedReality.InitializeAsync."); + throw new InvalidOperationException($"StereoKit is not initialized. Call SK.Initialize before calling MixedReality.{nameof(Initialize)}."); } // Create the spatial anchor helper - SpatialAnchorHelper = new SpatialAnchorHelper(await SpatialAnchorManager.RequestStoreAsync()); + SpatialAnchorHelper = new SpatialAnchorHelper(SpatialAnchorManager.RequestStoreAsync().AsTask().GetAwaiter().GetResult()); InitializeWorldCoordinateSystem(worldSpatialAnchor, regenerateDefaultWorldSpatialAnchorIfNeeded); @@ -61,11 +61,11 @@ public static async Task InitializeAsync(SpatialAnchor worldSpatialAnchor = null /// world coordinate system will be consistent across application sessions, unless the associated /// spatial anchor is modified or deleted. /// A spatial anchor to use for the world (may be null). - /// Flag indicating whether to regenerate and persist default world spatial anchor if currently persisted anchor fails to localize in the current environment (default: false). + /// Flag indicating whether to regenerate and persist default world spatial anchor if currently persisted anchor fails to localize in the current environment. /// private static void InitializeWorldCoordinateSystem(SpatialAnchor worldSpatialAnchor, bool regenerateDefaultWorldSpatialAnchorIfNeeded) { - SpatialAnchor TryCreateDefaultWorldSpatialAnchor(SpatialStationaryFrameOfReference world) + static SpatialAnchor TryCreateDefaultWorldSpatialAnchor(SpatialStationaryFrameOfReference world) { // Save the world spatial coordinate system WorldSpatialCoordinateSystem = world.CoordinateSystem; @@ -135,12 +135,13 @@ SpatialAnchor TryCreateDefaultWorldSpatialAnchor(SpatialStationaryFrameOfReferen // Query the pose of the world anchor. We use this pose for rendering correctly in the world, // and for transforming from world coordinates to StereoKit coordinates. StereoKitTransforms.WorldHierarchy = World.FromPerceptionAnchor(worldSpatialAnchor).ToMatrix(); - StereoKitTransforms.WorldToStereoKit = StereoKitTransforms.WorldHierarchy.ToCoordinateSystem(); + StereoKitTransforms.WorldToStereoKit = StereoKitTransforms.WorldHierarchy.Value.ToCoordinateSystem(); // Inverting gives us a coordinate system that can be used for transforming from StereoKit to world coordinates. StereoKitTransforms.StereoKitToWorld = StereoKitTransforms.WorldToStereoKit.Invert(); System.Diagnostics.Trace.WriteLine($"StereoKit origin: {StereoKitTransforms.StereoKitToWorld.Origin.X},{StereoKitTransforms.StereoKitToWorld.Origin.Y},{StereoKitTransforms.StereoKitToWorld.Origin.Z}"); + SK.AddStepper(new SpatialTransformsUpdater(worldSpatialAnchor, StereoKitTransforms.StereoKitToWorld.TryConvertPsiCoordinateSystemToSpatialCoordinateSystem())); // TODO: It would be nice if we could actually just shift the origin coordinate system in StereoKit // to the pose currently defined in StereoKitTransforms.WorldPose. @@ -154,5 +155,49 @@ SpatialAnchor TryCreateDefaultWorldSpatialAnchor(SpatialStationaryFrameOfReferen ////Renderer.CameraRoot = stereoKitTransform.Inverse; } } + + private class SpatialTransformsUpdater : IStepper + { + private readonly SpatialAnchor worldSpatialAnchor; + private readonly SpatialCoordinateSystem stereoKitSpatialCoordinateSystem; + + public SpatialTransformsUpdater(SpatialAnchor worldSpatialAnchor, SpatialCoordinateSystem stereoKitSpatialCoordinateSystem) + { + this.worldSpatialAnchor = worldSpatialAnchor; + this.stereoKitSpatialCoordinateSystem = stereoKitSpatialCoordinateSystem; + } + + /// + public bool Enabled => true; + + /// + public bool Initialize() => true; + + /// + public void Step() + { + if (this.stereoKitSpatialCoordinateSystem.TryConvertSpatialCoordinateSystemToPsiCoordinateSystem() is null) + { + StereoKitTransforms.StereoKitToWorld = null; + StereoKitTransforms.WorldToStereoKit = null; + StereoKitTransforms.WorldHierarchy = null; + } + else + { + // Query the pose of the world anchor. We use this pose for rendering correctly in the world, + // and for transforming from world coordinates to StereoKit coordinates. + StereoKitTransforms.WorldHierarchy = World.FromPerceptionAnchor(this.worldSpatialAnchor).ToMatrix(); + StereoKitTransforms.WorldToStereoKit = StereoKitTransforms.WorldHierarchy.Value.ToCoordinateSystem(); + + // Inverting gives us a coordinate system that can be used for transforming from StereoKit to world coordinates. + StereoKitTransforms.StereoKitToWorld = StereoKitTransforms.WorldToStereoKit.Invert(); + } + } + + /// + public void Shutdown() + { + } + } } } diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/Properties/AssemblyInfo.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/Properties/AssemblyInfo.cs index 43419d2e7..813089203 100644 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/Properties/AssemblyInfo.cs +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/Properties/AssemblyInfo.cs @@ -27,7 +27,7 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("0.17.52.1")] -[assembly: AssemblyFileVersion("0.17.52.1")] -[assembly: AssemblyInformationalVersion("0.17.52.1-beta")] +[assembly: AssemblyVersion("0.18.72.1")] +[assembly: AssemblyFileVersion("0.18.72.1")] +[assembly: AssemblyInformationalVersion("0.18.72.1-beta")] [assembly: ComVisible(false)] diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/Accelerometer.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/ResearchMode/Accelerometer.cs similarity index 95% rename from Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/Accelerometer.cs rename to Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/ResearchMode/Accelerometer.cs index d9c42b2e9..6c40abc2d 100644 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/Accelerometer.cs +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/ResearchMode/Accelerometer.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -namespace Microsoft.Psi.MixedReality +namespace Microsoft.Psi.MixedReality.ResearchMode { using HoloLens2ResearchMode; using Microsoft.Psi; diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/DepthCamera.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/ResearchMode/DepthCamera.cs similarity index 99% rename from Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/DepthCamera.cs rename to Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/ResearchMode/DepthCamera.cs index 0e01d9139..ec7e059c4 100644 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/DepthCamera.cs +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/ResearchMode/DepthCamera.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -namespace Microsoft.Psi.MixedReality +namespace Microsoft.Psi.MixedReality.ResearchMode { using System; using System.Diagnostics; diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/DepthCameraConfiguration.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/ResearchMode/DepthCameraConfiguration.cs similarity index 98% rename from Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/DepthCameraConfiguration.cs rename to Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/ResearchMode/DepthCameraConfiguration.cs index 144ac0a91..fba209cd5 100644 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/DepthCameraConfiguration.cs +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/ResearchMode/DepthCameraConfiguration.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -namespace Microsoft.Psi.MixedReality +namespace Microsoft.Psi.MixedReality.ResearchMode { using System; using HoloLens2ResearchMode; diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/Gyroscope.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/ResearchMode/Gyroscope.cs similarity index 95% rename from Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/Gyroscope.cs rename to Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/ResearchMode/Gyroscope.cs index c57ab20f1..37b789214 100644 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/Gyroscope.cs +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/ResearchMode/Gyroscope.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -namespace Microsoft.Psi.MixedReality +namespace Microsoft.Psi.MixedReality.ResearchMode { using HoloLens2ResearchMode; using Microsoft.Psi; diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/Magnetometer.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/ResearchMode/Magnetometer.cs similarity index 95% rename from Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/Magnetometer.cs rename to Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/ResearchMode/Magnetometer.cs index c955b9407..04baccdef 100644 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/Magnetometer.cs +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/ResearchMode/Magnetometer.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -namespace Microsoft.Psi.MixedReality +namespace Microsoft.Psi.MixedReality.ResearchMode { using HoloLens2ResearchMode; using Microsoft.Psi; diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/ResearchModeCamera.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/ResearchMode/ResearchModeCamera.cs similarity index 99% rename from Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/ResearchModeCamera.cs rename to Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/ResearchMode/ResearchModeCamera.cs index c17aa187a..528bc8ede 100644 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/ResearchModeCamera.cs +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/ResearchMode/ResearchModeCamera.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -namespace Microsoft.Psi.MixedReality +namespace Microsoft.Psi.MixedReality.ResearchMode { using System; using System.IO; diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/ResearchModeCameraConfiguration.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/ResearchMode/ResearchModeCameraConfiguration.cs similarity index 98% rename from Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/ResearchModeCameraConfiguration.cs rename to Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/ResearchMode/ResearchModeCameraConfiguration.cs index 0e525f017..9d8bc60a0 100644 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/ResearchModeCameraConfiguration.cs +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/ResearchMode/ResearchModeCameraConfiguration.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -namespace Microsoft.Psi.MixedReality +namespace Microsoft.Psi.MixedReality.ResearchMode { using System; using HoloLens2ResearchMode; diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/ResearchModeImu.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/ResearchMode/ResearchModeImu.cs similarity index 99% rename from Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/ResearchModeImu.cs rename to Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/ResearchMode/ResearchModeImu.cs index 98307c7ce..95dd77757 100644 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/ResearchModeImu.cs +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/ResearchMode/ResearchModeImu.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -namespace Microsoft.Psi.MixedReality +namespace Microsoft.Psi.MixedReality.ResearchMode { using System; using System.Linq; diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/VisibleLightCamera.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/ResearchMode/VisibleLightCamera.cs similarity index 98% rename from Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/VisibleLightCamera.cs rename to Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/ResearchMode/VisibleLightCamera.cs index 5990cf271..a20f45848 100644 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/VisibleLightCamera.cs +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/ResearchMode/VisibleLightCamera.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -namespace Microsoft.Psi.MixedReality +namespace Microsoft.Psi.MixedReality.ResearchMode { using System; using System.Diagnostics; diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/VisibleLightCameraConfiguration.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/ResearchMode/VisibleLightCameraConfiguration.cs similarity index 97% rename from Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/VisibleLightCameraConfiguration.cs rename to Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/ResearchMode/VisibleLightCameraConfiguration.cs index 37d70b3be..9ce1ff3d1 100644 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/VisibleLightCameraConfiguration.cs +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/ResearchMode/VisibleLightCameraConfiguration.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -namespace Microsoft.Psi.MixedReality +namespace Microsoft.Psi.MixedReality.ResearchMode { using System; using HoloLens2ResearchMode; diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/SceneUnderstanding.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/SceneUnderstanding.cs index 62330bfc7..1bcea5ea4 100644 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/SceneUnderstanding.cs +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/SceneUnderstanding.cs @@ -11,7 +11,6 @@ namespace Microsoft.Psi.MixedReality using Microsoft.MixedReality.SceneUnderstanding; using Microsoft.Psi.Components; using Microsoft.Psi.Spatial.Euclidean; - using StereoKit; using Windows.Perception.Spatial.Preview; /// @@ -132,7 +131,7 @@ Rectangle3D QuadToRectangle(SceneQuad quad, CoordinateSystem rectanglePose) } // origin is top-left of quad plane, so shift to be relative to the centroid (in 3D) - var placementFromCenter = new Vec3(placement.X - (quad.Extents.X / 2f), placement.Y - (quad.Extents.Y / 2f), 0); + var placementFromCenter = new Vector3(placement.X - (quad.Extents.X / 2f), placement.Y - (quad.Extents.Y / 2f), 0); return new Rectangle3D( rectanglePose.Transform(placementFromCenter.ToPoint3D()), diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/WinRT/GazeSensor.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/WinRT/GazeSensor.cs new file mode 100644 index 000000000..8fc519297 --- /dev/null +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/WinRT/GazeSensor.cs @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.Psi.MixedReality.WinRT +{ + using System; + using MathNet.Spatial.Euclidean; + using Microsoft.Psi.Components; + using Windows.Perception; + using Windows.Perception.People; + using Windows.UI.Input; + using Windows.UI.Input.Spatial; + + /// + /// Source component that emits head and/or eye gaze using WinRT. + /// + /// The origin of the eyes and head poses are between the user's eyes. + /// If emitting both eye and head gaze, the head poses are computed and emitted + /// in alignment with the timestamp of each eye gaze sample. + /// See here for more information about the WinRT APIs: + /// https://learn.microsoft.com/en-us/windows/mixed-reality/develop/native/gaze-in-directx. + public class GazeSensor : Generator + { + private readonly Pipeline pipeline; + private readonly GazeSensorConfiguration configuration; + + /// + /// Initializes a new instance of the class. + /// + /// The pipeline to add the component to. + /// The configuration for this component. + /// An optional name for the component. + public GazeSensor(Pipeline pipeline, GazeSensorConfiguration configuration = null, string name = nameof(GazeSensor)) + : base(pipeline, true, name) + { + this.pipeline = pipeline; + this.Eyes = this.pipeline.CreateEmitter(this, nameof(this.Eyes)); + this.Head = this.pipeline.CreateEmitter(this, nameof(this.Head)); + this.configuration = configuration ?? new GazeSensorConfiguration(); + + // If configured to emit eye gaze, make sure that eye gaze is supported and accessible on the device. + if (this.configuration.OutputEyeGaze) + { + this.pipeline.PipelineRun += (_, _) => + { + if (!EyesPose.IsSupported()) + { + throw new Exception("Eye gaze is not supported by the current headset."); + } + + var accessStatus = EyesPose.RequestAccessAsync().GetAwaiter().GetResult(); + if (accessStatus != GazeInputAccessStatus.Allowed) + { + throw new Exception($"Gaze Input access denied: {accessStatus}"); + } + }; + } + } + + /// + /// Gets the emitter for the eye gaze. + /// + /// The origin of the pose ray is between the user's eyes. + public Emitter Eyes { get; } + + /// + /// Gets the emitter for the head gaze. + /// + /// The origin of the pose is between the user's eyes. + public Emitter Head { get; } + + /// + protected override DateTime GenerateNext(DateTime currentTime) + { + // Get the current timestamp + var perceptionTimestamp = PerceptionTimestampHelper.FromHistoricalTargetTime(currentTime); + var originatingTime = this.pipeline.GetCurrentTimeFromElapsedTicks(perceptionTimestamp.SystemRelativeTargetTime.Ticks); + + // Query for the gaze + var spatialPointerPose = SpatialPointerPose.TryGetAtTimestamp(MixedReality.WorldSpatialCoordinateSystem, perceptionTimestamp); + var headPose = spatialPointerPose?.Head; + var eyesPose = spatialPointerPose?.Eyes; + + if (this.configuration.OutputEyeGaze && eyesPose is null) + { + // If configured to output eye gaze, but we received a null result, and thus have no timestamp from the + // eye tracking device, simply return without posting anything for the eyes or head. + return currentTime + this.configuration.Interval; + } + + // Eyes + if (this.configuration.OutputEyeGaze) + { + // Get the actual timestamp for this eye gaze result, as reported by the underlying eye tracker device + originatingTime = this.pipeline.GetCurrentTimeFromElapsedTicks(eyesPose.UpdateTimestamp.SystemRelativeTargetTime.Ticks); + + if (originatingTime > this.Eyes.LastEnvelope.OriginatingTime) + { + this.Eyes.Post(new Eyes(eyesPose.Gaze?.ToRay3D(), eyesPose.IsCalibrationValid), originatingTime); + } + + if (this.configuration.OutputHeadGaze) + { + // Use the actual time of the eye gaze result to re-query for the head pose at the exact same time. + headPose = SpatialPointerPose.TryGetAtTimestamp(MixedReality.WorldSpatialCoordinateSystem, eyesPose.UpdateTimestamp)?.Head; + } + } + + // Head + if (this.configuration.OutputHeadGaze && originatingTime > this.Head.LastEnvelope.OriginatingTime) + { + this.Head.Post(headPose?.ToCoordinateSystem(), originatingTime); + } + + return currentTime + this.configuration.Interval; + } + } +} diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/WinRT/GazeSensorConfiguration.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/WinRT/GazeSensorConfiguration.cs new file mode 100644 index 000000000..3433216fc --- /dev/null +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/WinRT/GazeSensorConfiguration.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.Psi.MixedReality.WinRT +{ + using System; + + /// + /// The configuration for the component. + /// + public class GazeSensorConfiguration + { + /// + /// Gets or sets a value indicating whether to emit head gaze poses. + /// + /// The origin of the pose is between the user's eyes. + public bool OutputHeadGaze { get; set; } = true; + + /// + /// Gets or sets a value indicating whether to emit eye gaze poses. + /// + /// The origin of the pose ray is between the user's eyes. + public bool OutputEyeGaze { get; set; } = true; + + /// + /// Gets or sets the desired interval for querying the gaze and emitting (default is 60 Hz). + /// + public TimeSpan Interval { get; set; } = TimeSpan.FromSeconds(1.0 / 60.0); + } +} \ No newline at end of file diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/WinRT/Operators.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/WinRT/Operators.cs new file mode 100644 index 000000000..d9baed80b --- /dev/null +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality.UniversalWindows/WinRT/Operators.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.Psi.MixedReality.WinRT +{ + using MathNet.Spatial.Euclidean; + using Windows.Perception.People; + using Windows.Perception.Spatial; + + /// + /// Implements operators. + /// + public static partial class Operators + { + /// + /// Converts a in HoloLens basis to a in \psi basis. + /// + /// The . + /// The . + public static Ray3D ToRay3D(this SpatialRay spatialRay) => new (spatialRay.Origin.ToPoint3D(), spatialRay.Direction.ToVector3D()); + + /// + /// Converts a in HoloLens basis to a pose in \psi basis. + /// + /// The . + /// The pose. + public static CoordinateSystem ToCoordinateSystem(this HeadPose headPose) + { + var forward = headPose.ForwardDirection.ToVector3D(); + var left = headPose.UpDirection.ToVector3D().CrossProduct(forward); + var up = forward.CrossProduct(left); + return new (headPose.Position.ToPoint3D(), forward.Normalize(), left.Normalize(), up.Normalize()); + } + } +} diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality.Visualization.Windows/OpenXR/HandVisualizationObject.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality.Visualization.Windows/OpenXR/HandVisualizationObject.cs new file mode 100644 index 000000000..dc629b8a8 --- /dev/null +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality.Visualization.Windows/OpenXR/HandVisualizationObject.cs @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.Psi.MixedReality.OpenXR.Visualization +{ + using System.Collections.Generic; + using System.ComponentModel; + using System.Linq; + using System.Runtime.Serialization; + using System.Windows.Media; + using MathNet.Spatial.Euclidean; + using Microsoft.Psi.MixedReality; + using Microsoft.Psi.MixedReality.OpenXR; + using Microsoft.Psi.Visualization.DataTypes; + using Microsoft.Psi.Visualization.VisualizationObjects; + using Xceed.Wpf.Toolkit.PropertyGrid.Attributes; + + /// + /// Implements a visualization object for . + /// + [VisualizationObject("Hand")] + public class HandVisualizationObject : ModelVisual3DVisualizationObject + { + private static readonly Dictionary<(HandJointIndex, HandJointIndex), bool> BoneEdges = Hand.Bones.ToDictionary(j => j, j => true); + + private bool showActiveOnly = false; + + /// + /// Initializes a new instance of the class. + /// + public HandVisualizationObject() + { + this.Joints = new () + { + EdgeDiameterMm = 10, + NodeRadiusMm = 7, + NodeColor = Colors.Silver, + EdgeColor = Colors.Gray, + }; + + this.Joints.RegisterChildPropertyChangedNotifications(this, nameof(this.Joints)); + this.UpdateVisibility(); + } + + /// + /// Gets or sets a value indicating whether to only show the hand when the tracker was active. + /// + [DataMember] + [Description("Indicates whether to only show hands when the tracker was active.")] + public bool ShowActiveOnly + { + get { return this.showActiveOnly; } + set { this.Set(nameof(this.ShowActiveOnly), ref this.showActiveOnly, value); } + } + + /// + /// Gets the graph visualization object for the joints. + /// + [ExpandableObject] + [DataMember] + [Description("Properties of the hand's joints.")] + public Point3DGraphVisualizationObject Joints { get; } + + /// + public override void NotifyPropertyChanged(string propertyName) + { + if (propertyName == nameof(this.Visible) || + propertyName == nameof(this.ShowActiveOnly)) + { + this.UpdateVisibility(); + } + } + + /// + public override void UpdateData() + { + this.UpdateJoints(); + this.UpdateVisibility(); + } + + private static Graph CreateJointGraph(Hand hand) + { + if (hand is null) + { + return null; + } + + var jointNodes = hand.Joints + .Select((j, i) => (i, j?.Origin)) + .Where(tuple => tuple.JointPosition.HasValue) + .ToDictionary(tuple => (HandJointIndex)tuple.JointIndex, tuple => tuple.JointPosition.Value); + + return new Graph(jointNodes, BoneEdges); + } + + private void UpdateJoints() + { + this.Joints.SetCurrentValue(this.SynthesizeMessage(CreateJointGraph(this.CurrentData))); + } + + private void UpdateVisibility() + { + var visible = this.Visible && this.CurrentData is not null; + this.UpdateChildVisibility(this.Joints.ModelView, this.ShowActiveOnly ? visible && this.CurrentData.IsActive : visible); + } + } +} diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality.Visualization.Windows/OpenXR/ProjectHandsToCamerasTask.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality.Visualization.Windows/OpenXR/ProjectHandsToCamerasTask.cs new file mode 100644 index 000000000..32c81bc8a --- /dev/null +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality.Visualization.Windows/OpenXR/ProjectHandsToCamerasTask.cs @@ -0,0 +1,140 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.Psi.MixedReality.OpenXR.Visualization +{ + using System.Collections.Generic; + using System.Linq; + using MathNet.Spatial.Euclidean; + using Microsoft.Psi; + using Microsoft.Psi.Common.Interpolators; + using Microsoft.Psi.Data; + using Microsoft.Psi.Imaging; + using Microsoft.Psi.PsiStudio.TypeSpec; + using Microsoft.Psi.Spatial.Euclidean; + + /// + /// Task that projects hands to camera views. + /// + [BatchProcessingTask( + "Project OpenXR Hands to Cameras", + Description = "Project hand joints based on all available streams of camera image views. Hands are first interpolated according to camera message times.", + OutputPartitionName = ProjectHandsToCamerasTaskConfiguration.DefaultPartitionName, + OutputStoreName = ProjectHandsToCamerasTaskConfiguration.DefaultStoreName)] + public class ProjectHandsToCamerasTask : BatchProcessingTask + { + /// + public override void Run(Pipeline pipeline, SessionImporter sessionImporter, Exporter exporter, ProjectHandsToCamerasTaskConfiguration configuration) + { + // First scan the importer for all hand streams and camera view streams + var cameraViewStreams = new List<(string streamName, string streamType)>(); + var handStreams = new Dictionary>(); + foreach (var partition in sessionImporter.PartitionImporters.Values) + { + foreach (var streamMeta in partition.AvailableStreams) + { + var streamType = TypeSpec.Simplify(streamMeta.TypeName); + if (streamType == nameof(ImageCameraView) || + streamType == nameof(EncodedImageCameraView) || + streamType == nameof(DepthImageCameraView) || + streamType == nameof(EncodedDepthImageCameraView)) + { + cameraViewStreams.Add((streamMeta.Name, streamType)); + } + else if (streamType == nameof(Hand)) + { + handStreams.Add(streamMeta.Name, partition.OpenStream(streamMeta.Name)); + } + else if (streamType == TypeSpec.Simplify(typeof((Hand, Hand)).FullName)) + { + var bothHands = partition.OpenStream<(Hand Left, Hand Right)>(streamMeta.Name); + handStreams.Add($"{streamMeta.Name}.Left", bothHands.Select(tuple => tuple.Left)); + handStreams.Add($"{streamMeta.Name}.Right", bothHands.Select(tuple => tuple.Right)); + } + } + } + + // Now process each stream (projecting hands to camera views) and persist results + foreach (var (cameraStreamName, cameraStreamType) in cameraViewStreams) + { + if (cameraStreamType == nameof(ImageCameraView)) + { + foreach (var handStream in handStreams) + { + ProjectHandToCamera>(handStream.Value, cameraStreamName, sessionImporter) + .Write($"{cameraStreamName}.{handStream.Key}", exporter); + } + } + else if (cameraStreamType == nameof(EncodedImageCameraView)) + { + foreach (var handStream in handStreams) + { + ProjectHandToCamera>(handStream.Value, cameraStreamName, sessionImporter) + .Write($"{cameraStreamName}.{handStream.Key}", exporter); + } + } + else if (cameraStreamType == nameof(DepthImageCameraView)) + { + foreach (var handStream in handStreams) + { + ProjectHandToCamera>(handStream.Value, cameraStreamName, sessionImporter) + .Write($"{cameraStreamName}.{handStream.Key}", exporter); + } + } + else if (cameraStreamType == nameof(EncodedDepthImageCameraView)) + { + foreach (var handStream in handStreams) + { + ProjectHandToCamera>(handStream.Value, cameraStreamName, sessionImporter) + .Write($"{cameraStreamName}.{handStream.Key}", exporter); + } + } + } + } + + private static IProducer> ProjectHandToCamera( + IProducer handStream, + string cameraStreamName, + SessionImporter sessionImporter) + where TCamera : CameraView + { + IProducer cameraStream = sessionImporter.OpenStream(cameraStreamName); + return cameraStream.Join( + handStream, + new AdjacentValuesInterpolator( + OpenXR.Operators.InterpolateHands, + false, + name: nameof(OpenXR.Operators.InterpolateHands))) + .Select(tuple => tuple.Item2.Joints.Where(j => j is not null).Select(j => tuple.Item1.GetPixelPosition(j.Origin, true)).ToList()); + } + } + +#pragma warning disable SA1402 // File may only contain a single type + + /// + /// Represents the configuration for the . + /// + public class ProjectHandsToCamerasTaskConfiguration : BatchProcessingTaskConfiguration + { + /// + /// Gets the default output partition name. + /// + public const string DefaultPartitionName = "ProjectedHands"; + + /// + /// Gets the default output store name. + /// + public const string DefaultStoreName = "ProjectedHands"; + + /// + /// Initializes a new instance of the class. + /// + public ProjectHandsToCamerasTaskConfiguration() + : base() + { + this.OutputPartitionName = DefaultPartitionName; + this.OutputStoreName = DefaultStoreName; + } + } +#pragma warning restore SA1402 // File may only contain a single type +} diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality.Visualization.Windows/ProjectHandsToCamerasTask.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality.Visualization.Windows/ProjectHandsToCamerasTask.cs deleted file mode 100644 index 6db03be3b..000000000 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality.Visualization.Windows/ProjectHandsToCamerasTask.cs +++ /dev/null @@ -1,152 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. - -namespace Microsoft.Psi.MixedReality.Visualization -{ - using System.Collections.Generic; - using System.ComponentModel; - using System.Linq; - using System.Runtime.Serialization; - using MathNet.Spatial.Euclidean; - using Microsoft.Psi; - using Microsoft.Psi.Common.Interpolators; - using Microsoft.Psi.Data; - using Microsoft.Psi.Imaging; - using Microsoft.Psi.MixedReality; - using Microsoft.Psi.Spatial.Euclidean; - using static Microsoft.Psi.MixedReality.Operators; - - /// - /// Task that projects hands to camera views. - /// - [BatchProcessingTask( - "Project Hands to Cameras", - Description = "Project hand joints based on all available streams of camera image views.", - OutputPartitionName = ProjectHandsToCamerasTaskConfiguration.DefaultPartitionName, - OutputStoreName = ProjectHandsToCamerasTaskConfiguration.DefaultStoreName)] - public class ProjectHandsToCamerasTask : BatchProcessingTask - { - /// - public override void Run(Pipeline pipeline, SessionImporter sessionImporter, Exporter exporter, ProjectHandsToCamerasTaskConfiguration configuration) - { - // Get the tracked left and right hand streams - var hands = sessionImporter.OpenStream<(Hand Left, Hand Right)>(configuration.HandsStreamName); - var leftHand = hands.Select(h => h.Left).Where(h => h is not null && h.IsTracked); - var rightHand = hands.Select(h => h.Right).Where(h => h is not null && h.IsTracked); - - foreach (var partition in sessionImporter.PartitionImporters.Values) - { - foreach (var streamMeta in partition.AvailableStreams) - { -#pragma warning disable SA1101 // Prefix local calls with this - void ProcessCameraStream() - where TCamera : CameraView - { - var cameraStream = partition.OpenStream(streamMeta.Name); - var resultsLeft = ProjectHandToCamera(leftHand, cameraStream, configuration.InterpolateHands); - var resultsRight = ProjectHandToCamera(rightHand, cameraStream, configuration.InterpolateHands); - var interpolated = configuration.InterpolateHands ? "Interpolated." : string.Empty; - resultsLeft.Select(tuple => tuple.ProjectedHand).Write($"{streamMeta.Name}.{interpolated}Projected.{configuration.HandsStreamName}.Left", exporter); - resultsRight.Select(tuple => tuple.ProjectedHand).Write($"{streamMeta.Name}.{interpolated}Projected.{configuration.HandsStreamName}.Right", exporter); - if (configuration.InterpolateHands) - { - resultsLeft.Select(tuple => tuple.InterpolatedHand).Write($"{streamMeta.Name}.{interpolated}{configuration.HandsStreamName}.Left", exporter); - resultsRight.Select(tuple => tuple.InterpolatedHand).Write($"{streamMeta.Name}.{interpolated}{configuration.HandsStreamName}.Right", exporter); - } - } - - var streamType = streamMeta.TypeName.Split(',')[0]; - if (string.Equals(streamType, typeof(ImageCameraView).FullName)) - { - ProcessCameraStream>(); - } - else if (string.Equals(streamType, typeof(EncodedImageCameraView).FullName)) - { - ProcessCameraStream>(); - } - else if (string.Equals(streamType, typeof(DepthImageCameraView).FullName)) - { - ProcessCameraStream>(); - } - else if (string.Equals(streamType, typeof(EncodedDepthImageCameraView).FullName)) - { - ProcessCameraStream>(); - } -#pragma warning restore SA1101 // Prefix local calls with this - } - } - } - - private static IProducer<(List ProjectedHand, Hand InterpolatedHand)> ProjectHandToCamera( - IProducer handStream, - IProducer cameraStream, - bool interpolateHand) - where TCamera : CameraView - { - static List ProjectJoints(Hand hand, TCamera camera) - => hand.Joints.Where(j => j is not null).Select(j => camera.GetPixelPosition(j.Origin, true)).ToList(); - - return interpolateHand ? - cameraStream.Join(handStream, new AdjacentValuesInterpolator(InterpolateHands, false, name: nameof(InterpolateHands))) - .Select(tuple => (ProjectJoints(tuple.Item2, tuple.Item1), tuple.Item2)) : - handStream.Join(cameraStream, RelativeTimeInterval.Infinite) - .Select(tuple => (ProjectJoints(tuple.Item1, tuple.Item2), tuple.Item1)); - } - } - -#pragma warning disable SA1402 // File may only contain a single type - - /// - /// Represents the configuration for the . - /// - public class ProjectHandsToCamerasTaskConfiguration : BatchProcessingTaskConfiguration - { - /// - /// Gets the default output partition name. - /// - public const string DefaultPartitionName = "ProjectedHands"; - - /// - /// Gets the default output store name. - /// - public const string DefaultStoreName = "ProjectedHands"; - - private string handsStreamName = "Hands"; - private bool interpolateHands = false; - - /// - /// Initializes a new instance of the class. - /// - public ProjectHandsToCamerasTaskConfiguration() - : base() - { - this.OutputPartitionName = DefaultPartitionName; - this.OutputStoreName = DefaultStoreName; - } - - /// - /// Gets or sets the name of the hands source stream. - /// - [DataMember] - [DisplayName("Hands Stream Name")] - [Description("The name of the hands source stream.")] - public string HandsStreamName - { - get => this.handsStreamName; - set { this.Set(nameof(this.HandsStreamName), ref this.handsStreamName, value); } - } - - /// - /// Gets or sets a value indicating whether to interpolate hands to the timestamps of camera messages. - /// - [DataMember] - [DisplayName("Interpolate Hands")] - [Description("Indicates whether to interpolate hands to the camera message times.")] - public bool InterpolateHands - { - get => this.interpolateHands; - set { this.Set(nameof(this.InterpolateHands), ref this.interpolateHands, value); } - } - } -#pragma warning restore SA1402 // File may only contain a single type -} diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality.Visualization.Windows/HandVisualizationObject.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality.Visualization.Windows/StereoKit/HandVisualizationObject.cs similarity index 84% rename from Sources/MixedReality/Microsoft.Psi.MixedReality.Visualization.Windows/HandVisualizationObject.cs rename to Sources/MixedReality/Microsoft.Psi.MixedReality.Visualization.Windows/StereoKit/HandVisualizationObject.cs index ab11657eb..126671cf0 100644 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality.Visualization.Windows/HandVisualizationObject.cs +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality.Visualization.Windows/StereoKit/HandVisualizationObject.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -namespace Microsoft.Psi.MixedReality.Visualization +namespace Microsoft.Psi.MixedReality.StereoKit.Visualization { using System.Collections.Generic; using System.ComponentModel; @@ -10,6 +10,7 @@ namespace Microsoft.Psi.MixedReality.Visualization using System.Windows.Media; using MathNet.Spatial.Euclidean; using Microsoft.Psi.MixedReality; + using Microsoft.Psi.MixedReality.StereoKit; using Microsoft.Psi.Visualization.DataTypes; using Microsoft.Psi.Visualization.VisualizationObjects; using Xceed.Wpf.Toolkit.PropertyGrid.Attributes; @@ -29,11 +30,13 @@ public class HandVisualizationObject : ModelVisual3DVisualizationObject /// public HandVisualizationObject() { - this.Joints = new (); - this.Joints.EdgeDiameterMm = 10; - this.Joints.NodeRadiusMm = 7; - this.Joints.NodeColor = Colors.Silver; - this.Joints.EdgeColor = Colors.Gray; + this.Joints = new () + { + EdgeDiameterMm = 10, + NodeRadiusMm = 7, + NodeColor = Colors.Silver, + EdgeColor = Colors.Gray, + }; this.Joints.RegisterChildPropertyChangedNotifications(this, nameof(this.Joints)); this.UpdateVisibility(); @@ -77,7 +80,12 @@ public override void UpdateData() private static Graph CreateJointGraph(Hand hand) { - var jointNodes = hand?.Joints + if (hand is null) + { + return null; + } + + var jointNodes = hand.Joints .Select((j, i) => (i, j?.Origin)) .Where(tuple => tuple.JointPosition.HasValue) .ToDictionary(tuple => (HandJointIndex)tuple.JointIndex, tuple => tuple.JointPosition.Value); @@ -93,9 +101,7 @@ private void UpdateJoints() private void UpdateVisibility() { var visible = this.Visible && this.CurrentData is not null; - var trackedVisible = this.ShowTrackedOnly ? visible && this.CurrentData.IsTracked : visible; - - this.UpdateChildVisibility(this.Joints.ModelView, trackedVisible); + this.UpdateChildVisibility(this.Joints.ModelView, this.ShowTrackedOnly ? visible && this.CurrentData.IsTracked : visible); } } } diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality.Visualization.Windows/StereoKit/ProjectHandsToCamerasTask.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality.Visualization.Windows/StereoKit/ProjectHandsToCamerasTask.cs new file mode 100644 index 000000000..9a8aca9d5 --- /dev/null +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality.Visualization.Windows/StereoKit/ProjectHandsToCamerasTask.cs @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.Psi.MixedReality.StereoKit.Visualization +{ + using System.Collections.Generic; + using System.Linq; + using MathNet.Spatial.Euclidean; + using Microsoft.Psi; + using Microsoft.Psi.Common.Interpolators; + using Microsoft.Psi.Data; + using Microsoft.Psi.Imaging; + using Microsoft.Psi.MixedReality.StereoKit; + using Microsoft.Psi.PsiStudio.TypeSpec; + using Microsoft.Psi.Spatial.Euclidean; + + /// + /// Task that projects hands to camera views. + /// + [BatchProcessingTask( + "Project StereoKit Hands to Cameras", + Description = "Project hand joints based on all available streams of camera image views. Hands are first interpolated according to camera message times.", + OutputPartitionName = ProjectHandsToCamerasTaskConfiguration.DefaultPartitionName, + OutputStoreName = ProjectHandsToCamerasTaskConfiguration.DefaultStoreName)] + public class ProjectHandsToCamerasTask : BatchProcessingTask + { + /// + public override void Run(Pipeline pipeline, SessionImporter sessionImporter, Exporter exporter, ProjectHandsToCamerasTaskConfiguration configuration) + { + // First scan the importer for all hand streams and camera view streams + var cameraViewStreams = new List<(string streamName, string streamType)>(); + var handStreams = new Dictionary>(); + foreach (var partition in sessionImporter.PartitionImporters.Values) + { + foreach (var streamMeta in partition.AvailableStreams) + { + var streamType = TypeSpec.Simplify(streamMeta.TypeName); + if (streamType == nameof(ImageCameraView) || + streamType == nameof(EncodedImageCameraView) || + streamType == nameof(DepthImageCameraView) || + streamType == nameof(EncodedDepthImageCameraView)) + { + cameraViewStreams.Add((streamMeta.Name, streamType)); + } + else if (streamType == nameof(Hand)) + { + handStreams.Add(streamMeta.Name, partition.OpenStream(streamMeta.Name)); + } + else if (streamType == TypeSpec.Simplify(typeof((Hand, Hand)).FullName)) + { + var bothHands = partition.OpenStream<(Hand Left, Hand Right)>(streamMeta.Name); + handStreams.Add($"{streamMeta.Name}.Left", bothHands.Select(tuple => tuple.Left)); + handStreams.Add($"{streamMeta.Name}.Right", bothHands.Select(tuple => tuple.Right)); + } + } + } + + // Now process each stream (projecting hands to camera views) and persist results + foreach (var (cameraStreamName, cameraStreamType) in cameraViewStreams) + { + if (cameraStreamType == nameof(ImageCameraView)) + { + foreach (var handStream in handStreams) + { + ProjectHandToCamera>(handStream.Value, cameraStreamName, sessionImporter) + .Write($"{cameraStreamName}.{handStream.Key}", exporter); + } + } + else if (cameraStreamType == nameof(EncodedImageCameraView)) + { + foreach (var handStream in handStreams) + { + ProjectHandToCamera>(handStream.Value, cameraStreamName, sessionImporter) + .Write($"{cameraStreamName}.{handStream.Key}", exporter); + } + } + else if (cameraStreamType == nameof(DepthImageCameraView)) + { + foreach (var handStream in handStreams) + { + ProjectHandToCamera>(handStream.Value, cameraStreamName, sessionImporter) + .Write($"{cameraStreamName}.{handStream.Key}", exporter); + } + } + else if (cameraStreamType == nameof(EncodedDepthImageCameraView)) + { + foreach (var handStream in handStreams) + { + ProjectHandToCamera>(handStream.Value, cameraStreamName, sessionImporter) + .Write($"{cameraStreamName}.{handStream.Key}", exporter); + } + } + } + } + + private static IProducer> ProjectHandToCamera( + IProducer handStream, + string cameraStreamName, + SessionImporter sessionImporter) + where TCamera : CameraView + { + IProducer cameraStream = sessionImporter.OpenStream(cameraStreamName); + return cameraStream.Join( + handStream, + new AdjacentValuesInterpolator( + StereoKit.Operators.InterpolateHands, + false, + name: nameof(StereoKit.Operators.InterpolateHands))) + .Select(tuple => tuple.Item2.Joints.Where(j => j is not null).Select(j => tuple.Item1.GetPixelPosition(j.Origin, true)).ToList()); + } + } + +#pragma warning disable SA1402 // File may only contain a single type + + /// + /// Represents the configuration for the . + /// + public class ProjectHandsToCamerasTaskConfiguration : BatchProcessingTaskConfiguration + { + /// + /// Gets the default output partition name. + /// + public const string DefaultPartitionName = "ProjectedHands"; + + /// + /// Gets the default output store name. + /// + public const string DefaultStoreName = "ProjectedHands"; + + /// + /// Initializes a new instance of the class. + /// + public ProjectHandsToCamerasTaskConfiguration() + : base() + { + this.OutputPartitionName = DefaultPartitionName; + this.OutputStoreName = DefaultStoreName; + } + } +#pragma warning restore SA1402 // File may only contain a single type +} diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality/Microsoft.Psi.MixedReality.csproj b/Sources/MixedReality/Microsoft.Psi.MixedReality/Microsoft.Psi.MixedReality.csproj index 047612e87..7e72da776 100644 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality/Microsoft.Psi.MixedReality.csproj +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality/Microsoft.Psi.MixedReality.csproj @@ -27,7 +27,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality/OpenXR/Hand.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality/OpenXR/Hand.cs new file mode 100644 index 000000000..05e4c9c52 --- /dev/null +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality/OpenXR/Hand.cs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.Psi.MixedReality.OpenXR +{ + using System.Collections.Generic; + using MathNet.Spatial.Euclidean; + + /// + /// Represents one of the user's hands, as produced by the OpenXR-based component. + /// + public class Hand + { + /// + /// Initializes a new instance of the class. + /// + /// Value indicating whether or not the hand tracker was active for this hand result. + /// Finger joint poses (index by ). + /// Values indicating whether or not the pose for each joint is valid. + /// Values indicating whether or not the pose for each joint was tracked. (If false, the pose was inferred). + public Hand(bool isActive, CoordinateSystem[] joints, bool[] jointsValid, bool[] jointsTracked) + { + this.IsActive = isActive; + this.Joints = joints; + this.JointsValid = jointsValid; + this.JointsTracked = jointsTracked; + } + + /// + /// Gets an empty HandXR instance. + /// + public static Hand Empty => new ( + false, + new CoordinateSystem[(int)HandJointIndex.MaxIndex], + new bool[(int)HandJointIndex.MaxIndex], + new bool[(int)HandJointIndex.MaxIndex]); + + /// + /// Gets the definition of bones connecting the hand joints. + /// + public static List<(HandJointIndex Start, HandJointIndex End)> Bones => new () + { + (HandJointIndex.Wrist, HandJointIndex.Palm), + + (HandJointIndex.Wrist, HandJointIndex.ThumbMetacarpal), + (HandJointIndex.ThumbMetacarpal, HandJointIndex.ThumbProximal), + (HandJointIndex.ThumbProximal, HandJointIndex.ThumbDistal), + (HandJointIndex.ThumbDistal, HandJointIndex.ThumbTip), + + (HandJointIndex.Wrist, HandJointIndex.IndexMetacarpal), + (HandJointIndex.IndexMetacarpal, HandJointIndex.IndexProximal), + (HandJointIndex.IndexProximal, HandJointIndex.IndexIntermediate), + (HandJointIndex.IndexIntermediate, HandJointIndex.IndexDistal), + (HandJointIndex.IndexDistal, HandJointIndex.IndexTip), + + (HandJointIndex.Wrist, HandJointIndex.MiddleMetacarpal), + (HandJointIndex.MiddleMetacarpal, HandJointIndex.MiddleProximal), + (HandJointIndex.MiddleProximal, HandJointIndex.MiddleIntermediate), + (HandJointIndex.MiddleIntermediate, HandJointIndex.MiddleDistal), + (HandJointIndex.MiddleDistal, HandJointIndex.MiddleTip), + + (HandJointIndex.Wrist, HandJointIndex.RingMetacarpal), + (HandJointIndex.RingMetacarpal, HandJointIndex.RingProximal), + (HandJointIndex.RingProximal, HandJointIndex.RingIntermediate), + (HandJointIndex.RingIntermediate, HandJointIndex.RingDistal), + (HandJointIndex.RingDistal, HandJointIndex.RingTip), + + (HandJointIndex.Wrist, HandJointIndex.PinkyMetacarpal), + (HandJointIndex.PinkyMetacarpal, HandJointIndex.PinkyProximal), + (HandJointIndex.PinkyProximal, HandJointIndex.PinkyIntermediate), + (HandJointIndex.PinkyIntermediate, HandJointIndex.PinkyDistal), + (HandJointIndex.PinkyDistal, HandJointIndex.PinkyTip), + }; + + /// + /// Gets a value indicating whether or not the hand tracker was active for this hand result. + /// + /// + /// If false, it indicates the hand tracker did not detect the hand input, and all joint poses are invalid. + public bool IsActive { get; private set; } + + /// + /// Gets finger joint poses in \psi basis (indexed by ). + /// + public CoordinateSystem[] Joints { get; private set; } + + /// + /// Gets values indicating whether or not the pose for each joint is valid. + /// + public bool[] JointsValid { get; private set; } + + /// + /// Gets values indicating whether or not the pose for each joint was tracked. (If false, the pose was inferred). + /// + public bool[] JointsTracked { get; private set; } + + /// + /// Gets the joint in \psi basis specified by a . + /// + /// The joint index. + /// The corresponding joint. + public CoordinateSystem this[HandJointIndex handJointIndex] => this.Joints[(int)handJointIndex]; + } +} diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality/OpenXR/HandsSensor.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality/OpenXR/HandsSensor.cs new file mode 100644 index 000000000..e8138637b --- /dev/null +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality/OpenXR/HandsSensor.cs @@ -0,0 +1,222 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.Psi.MixedReality.OpenXR +{ + using System; + using System.Linq; + using System.Runtime.InteropServices; + using global::StereoKit; + using MathNet.Spatial.Euclidean; + using Microsoft.Psi.Components; + using Microsoft.Psi.MixedReality.StereoKit; + + /// + /// Source component that produces streams containing information about the tracked hands directly from OpenXR. + /// + public class HandsSensor : Generator, IProducer<(Hand Left, Hand Right)>, IDisposable + { + private static readonly long AhatFrameWidthTicks = TimeSpan.FromSeconds(1.0 / 45.0).Ticks; + + // OpenXR functions + private static readonly XR_xrCreateHandTrackerEXT OpenXrCreateHandTrackerEXT; + private static readonly XR_xrDestroyHandTrackerEXT OpenXrDestroyHandTrackerEXT; + private static readonly XR_xrLocateHandJointsEXT OpenXrLocateHandJointsEXT; + + private readonly Pipeline pipeline; + private readonly TimeSpan interval; + + // Variables for managing joint location data + private readonly int jointLocationSizeBytes; + private XrHandJointLocationsEXT leftHandJointLocationsXR; + private XrHandJointLocationsEXT rightHandJointLocationsXR; + + // Handles for the underlying hand trackers + private ulong leftHandTrackerHandle; + private ulong rightHandTrackerHandle; + + private Emitter leftHandEmitter = null; + private Emitter rightHandEmitter = null; + + static HandsSensor() + { + if (!SK.IsInitialized) + { + throw new InvalidOperationException($"Cannot initialize any {nameof(HandsSensor)} component before calling SK.Initialize."); + } + + if (Backend.XRType != BackendXRType.OpenXR) + { + throw new InvalidOperationException($"Cannot use {nameof(HandsSensor)} component if the backend XR type is not OpenXR."); + } + + // Load the necessary OpenXR functions via StereoKit. + OpenXrCreateHandTrackerEXT = Backend.OpenXR.GetFunction("xrCreateHandTrackerEXT"); + OpenXrDestroyHandTrackerEXT = Backend.OpenXR.GetFunction("xrDestroyHandTrackerEXT"); + OpenXrLocateHandJointsEXT = Backend.OpenXR.GetFunction("xrLocateHandJointsEXT"); + } + + /// + /// Initializes a new instance of the class. + /// + /// The pipeline to add the component to. + /// Interval for emitting hands (default is 45Hz). + /// An optional name for the component. + public HandsSensor(Pipeline pipeline, TimeSpan interval = default, string name = nameof(HandsSensor)) + : base(pipeline, true, name) + { + this.pipeline = pipeline; + this.Out = this.pipeline.CreateEmitter<(Hand Left, Hand Right)>(this, nameof(this.Out)); + this.interval = interval == default ? TimeSpan.FromTicks(AhatFrameWidthTicks) : interval; + + this.jointLocationSizeBytes = Marshal.SizeOf(); + this.leftHandJointLocationsXR = new XrHandJointLocationsEXT + { + Type = XrStructureType.XR_TYPE_HAND_JOINT_LOCATIONS_EXT, + JointCount = (uint)HandJointIndex.MaxIndex, + JointLocations = Marshal.AllocCoTaskMem(this.jointLocationSizeBytes * (int)HandJointIndex.MaxIndex), + }; + + this.rightHandJointLocationsXR = new XrHandJointLocationsEXT + { + Type = XrStructureType.XR_TYPE_HAND_JOINT_LOCATIONS_EXT, + JointCount = (uint)HandJointIndex.MaxIndex, + JointLocations = Marshal.AllocCoTaskMem(this.jointLocationSizeBytes * (int)HandJointIndex.MaxIndex), + }; + + this.pipeline.PipelineRun += (_, _) => + { + // Open underlying OpenXR hand trackers and create handles. + var leftHandCreateInfo = new XrHandTrackerCreateInfoEXT + { + Type = XrStructureType.XR_TYPE_HAND_TRACKER_CREATE_INFO_EXT, + Hand = XrHandEXT.XR_HAND_LEFT_EXT, + HandJointSet = XrHandJointSetEXT.XR_HAND_JOINT_SET_DEFAULT_EXT, + }; + + var rightHandCreateInfo = new XrHandTrackerCreateInfoEXT + { + Type = XrStructureType.XR_TYPE_HAND_TRACKER_CREATE_INFO_EXT, + Hand = XrHandEXT.XR_HAND_RIGHT_EXT, + HandJointSet = XrHandJointSetEXT.XR_HAND_JOINT_SET_DEFAULT_EXT, + }; + + var leftResult = OpenXrCreateHandTrackerEXT(Backend.OpenXR.Session, ref leftHandCreateInfo, out this.leftHandTrackerHandle); + var rightResult = OpenXrCreateHandTrackerEXT(Backend.OpenXR.Session, ref rightHandCreateInfo, out this.rightHandTrackerHandle); + + if (leftResult != XrResult.XR_SUCCESS || rightResult != XrResult.XR_SUCCESS) + { + throw new Exception("Error creating hand trackers.\n" + + $"OpenXR result code for left hand tracker: {leftResult}\n" + + $"OpenXR result code for right hand tracker: {rightResult}"); + } + }; + } + + private delegate XrResult XR_xrCreateHandTrackerEXT(ulong session, ref XrHandTrackerCreateInfoEXT createInfo, out ulong handTracker); + + private delegate XrResult XR_xrDestroyHandTrackerEXT(ulong handTracker); + + private delegate XrResult XR_xrLocateHandJointsEXT(ulong handTracker, ref XrHandJointsLocateInfoEXT locateInfo, ref XrHandJointLocationsEXT locations); + + /// + public Emitter<(Hand Left, Hand Right)> Out { get; } + + /// + /// Gets the stream of left hand information. + /// + public Emitter Left => this.leftHandEmitter ??= this.Out.Select( + hands => hands.Left, + DeliveryPolicy.SynchronousOrThrottle, + "SelectLeftHand").Out; + + /// + /// Gets the stream of right hand information. + /// + public Emitter Right => this.rightHandEmitter ??= this.Out.Select( + hands => hands.Right, + DeliveryPolicy.SynchronousOrThrottle, + "SelectRightHand").Out; + + /// + public void Dispose() + { + // Free up memory and destroy the hand tracker handles. + Marshal.FreeCoTaskMem(this.leftHandJointLocationsXR.JointLocations); + Marshal.FreeCoTaskMem(this.rightHandJointLocationsXR.JointLocations); + OpenXrDestroyHandTrackerEXT(this.leftHandTrackerHandle); + OpenXrDestroyHandTrackerEXT(this.rightHandTrackerHandle); + } + + /// + protected override DateTime GenerateNext(DateTime currentTime) + { + // HoloLens 2 computes hand tracking from the AHAT (articulated hand tracking) + // depth sensor stream (45 FPS). To ensure that the underlying hand tracker has + // a computed result ready, query at a time of 1 Ahat frame in the past. + var queryTimeTicks = TimeHelper.GetCurrentTimeHnsTicks() - AhatFrameWidthTicks; + + // The proper originating time to associate with the result appears to be + // 1 Ahat frame length subtracted from the actual query time. + var originatingTime = this.pipeline.GetCurrentTimeFromElapsedTicks(queryTimeTicks - AhatFrameWidthTicks); + + var locateInfo = new XrHandJointsLocateInfoEXT + { + Type = XrStructureType.XR_TYPE_HAND_JOINTS_LOCATE_INFO_EXT, + BaseSpace = Backend.OpenXR.Space, + Time = TimeHelper.ConvertHnsTicksToXrTime(queryTimeTicks), + }; + + // Query the left hand tracker. + var leftHand = default(Hand); + if (OpenXrLocateHandJointsEXT(this.leftHandTrackerHandle, ref locateInfo, ref this.leftHandJointLocationsXR) == XrResult.XR_SUCCESS) + { + leftHand = this.CreateHand(this.leftHandJointLocationsXR); + } + + // Query the right hand tracker. + var rightHand = default(Hand); + if (OpenXrLocateHandJointsEXT(this.rightHandTrackerHandle, ref locateInfo, ref this.rightHandJointLocationsXR) == XrResult.XR_SUCCESS) + { + rightHand = this.CreateHand(this.rightHandJointLocationsXR); + } + + this.Out.Post((leftHand, rightHand), originatingTime); + return currentTime + this.interval; + } + + private Hand CreateHand(XrHandJointLocationsEXT handJointLocationsXR) + { + // Since StereoKitToWorld could be updated by the Stepper method (which is not mutually exclusive with + // this method), we capture its value once at the start and use it to compute all the joint transforms. + var stereoKitToWorld = StereoKitTransforms.StereoKitToWorld; + if (stereoKitToWorld is null) + { + return null; + } + + bool isActive = handJointLocationsXR.IsActive; + var handJoints = new CoordinateSystem[(int)HandJointIndex.MaxIndex]; + var validJoints = new bool[(int)HandJointIndex.MaxIndex]; + var trackedJoints = new bool[(int)HandJointIndex.MaxIndex]; + for (int i = 0; i < (int)HandJointIndex.MaxIndex; i++) + { + // Read the joint data + var ptr = handJointLocationsXR.JointLocations + (i * this.jointLocationSizeBytes); + var jointLocationXR = Marshal.PtrToStructure(ptr); + + // Check if the joint pose is marked as valid. + validJoints[i] = (jointLocationXR.LocationFlags & XrSpaceLocationFlags.XR_SPACE_LOCATION_POSITION_VALID_BIT) != 0; + + // Check if the joint pose is marked as tracked. + trackedJoints[i] = (jointLocationXR.LocationFlags & XrSpaceLocationFlags.XR_SPACE_LOCATION_POSITION_TRACKED_BIT) != 0; + + // Construct the joint pose + var pose = new Pose(jointLocationXR.Pose.Position, jointLocationXR.Pose.Orientation); + handJoints[i] = pose.ToCoordinateSystem().TransformBy(stereoKitToWorld); + } + + return new Hand(isActive, handJoints, validJoints, trackedJoints); + } + } +} diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality/OpenXR/OpenXR.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality/OpenXR/OpenXR.cs new file mode 100644 index 000000000..cb37d77cb --- /dev/null +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality/OpenXR/OpenXR.cs @@ -0,0 +1,684 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +#pragma warning disable SA1649 // File name should match first type name +#pragma warning disable SA1602 // Enumeration items should be documented +#pragma warning disable SA1310 // Field names should not contain underscore +namespace Microsoft.Psi.MixedReality.OpenXR +{ + using System; + using System.Runtime.InteropServices; + using global::StereoKit; + + /// + /// Describes which hand the tracker is tracking. + /// + /// + /// https://www.khronos.org/registry/OpenXR/specs/1.0/man/html/XrHandEXT.html. + /// + internal enum XrHandEXT + { + XR_HAND_LEFT_EXT = 1, + XR_HAND_RIGHT_EXT = 2, + XR_HAND_MAX_ENUM_EXT = 0x7FFFFFFF, + } + + /// + /// The set of hand joints to track. + /// + /// + /// https://www.khronos.org/registry/OpenXR/specs/1.0/man/html/XrHandJointSetEXT.html. + /// + internal enum XrHandJointSetEXT + { + XR_HAND_JOINT_SET_DEFAULT_EXT = 0, + XR_HAND_JOINT_SET_MAX_ENUM_EXT = 0x7FFFFFFF, + } + + /// + /// Result codes. + /// + /// + /// https://www.khronos.org/registry/OpenXR/specs/1.0/man/html/XrResult.html. + /// + internal enum XrResult + { + XR_SUCCESS = 0, + XR_TIMEOUT_EXPIRED = 1, + XR_SESSION_LOSS_PENDING = 3, + XR_EVENT_UNAVAILABLE = 4, + XR_SPACE_BOUNDS_UNAVAILABLE = 7, + XR_SESSION_NOT_FOCUSED = 8, + XR_FRAME_DISCARDED = 9, + XR_ERROR_VALIDATION_FAILURE = -1, + XR_ERROR_RUNTIME_FAILURE = -2, + XR_ERROR_OUT_OF_MEMORY = -3, + XR_ERROR_API_VERSION_UNSUPPORTED = -4, + XR_ERROR_INITIALIZATION_FAILED = -6, + XR_ERROR_FUNCTION_UNSUPPORTED = -7, + XR_ERROR_FEATURE_UNSUPPORTED = -8, + XR_ERROR_EXTENSION_NOT_PRESENT = -9, + XR_ERROR_LIMIT_REACHED = -10, + XR_ERROR_SIZE_INSUFFICIENT = -11, + XR_ERROR_HANDLE_INVALID = -12, + XR_ERROR_INSTANCE_LOST = -13, + XR_ERROR_SESSION_RUNNING = -14, + XR_ERROR_SESSION_NOT_RUNNING = -16, + XR_ERROR_SESSION_LOST = -17, + XR_ERROR_SYSTEM_INVALID = -18, + XR_ERROR_PATH_INVALID = -19, + XR_ERROR_PATH_COUNT_EXCEEDED = -20, + XR_ERROR_PATH_FORMAT_INVALID = -21, + XR_ERROR_PATH_UNSUPPORTED = -22, + XR_ERROR_LAYER_INVALID = -23, + XR_ERROR_LAYER_LIMIT_EXCEEDED = -24, + XR_ERROR_SWAPCHAIN_RECT_INVALID = -25, + XR_ERROR_SWAPCHAIN_FORMAT_UNSUPPORTED = -26, + XR_ERROR_ACTION_TYPE_MISMATCH = -27, + XR_ERROR_SESSION_NOT_READY = -28, + XR_ERROR_SESSION_NOT_STOPPING = -29, + XR_ERROR_TIME_INVALID = -30, + XR_ERROR_REFERENCE_SPACE_UNSUPPORTED = -31, + XR_ERROR_FILE_ACCESS_ERROR = -32, + XR_ERROR_FILE_CONTENTS_INVALID = -33, + XR_ERROR_FORM_FACTOR_UNSUPPORTED = -34, + XR_ERROR_FORM_FACTOR_UNAVAILABLE = -35, + XR_ERROR_API_LAYER_NOT_PRESENT = -36, + XR_ERROR_CALL_ORDER_INVALID = -37, + XR_ERROR_GRAPHICS_DEVICE_INVALID = -38, + XR_ERROR_POSE_INVALID = -39, + XR_ERROR_INDEX_OUT_OF_RANGE = -40, + XR_ERROR_VIEW_CONFIGURATION_TYPE_UNSUPPORTED = -41, + XR_ERROR_ENVIRONMENT_BLEND_MODE_UNSUPPORTED = -42, + XR_ERROR_NAME_DUPLICATED = -44, + XR_ERROR_NAME_INVALID = -45, + XR_ERROR_ACTIONSET_NOT_ATTACHED = -46, + XR_ERROR_ACTIONSETS_ALREADY_ATTACHED = -47, + XR_ERROR_LOCALIZED_NAME_DUPLICATED = -48, + XR_ERROR_LOCALIZED_NAME_INVALID = -49, + XR_ERROR_GRAPHICS_REQUIREMENTS_CALL_MISSING = -50, + XR_ERROR_RUNTIME_UNAVAILABLE = -51, + XR_ERROR_ANDROID_THREAD_SETTINGS_ID_INVALID_KHR = -1000003000, + XR_ERROR_ANDROID_THREAD_SETTINGS_FAILURE_KHR = -1000003001, + XR_ERROR_CREATE_SPATIAL_ANCHOR_FAILED_MSFT = -1000039001, + XR_ERROR_SECONDARY_VIEW_CONFIGURATION_TYPE_NOT_ENABLED_MSFT = -1000053000, + XR_ERROR_CONTROLLER_MODEL_KEY_INVALID_MSFT = -1000055000, + XR_ERROR_REPROJECTION_MODE_UNSUPPORTED_MSFT = -1000066000, + XR_ERROR_COMPUTE_NEW_SCENE_NOT_COMPLETED_MSFT = -1000097000, + XR_ERROR_SCENE_COMPONENT_ID_INVALID_MSFT = -1000097001, + XR_ERROR_SCENE_COMPONENT_TYPE_MISMATCH_MSFT = -1000097002, + XR_ERROR_SCENE_MESH_BUFFER_ID_INVALID_MSFT = -1000097003, + XR_ERROR_SCENE_COMPUTE_FEATURE_INCOMPATIBLE_MSFT = -1000097004, + XR_ERROR_SCENE_COMPUTE_CONSISTENCY_MISMATCH_MSFT = -1000097005, + XR_ERROR_DISPLAY_REFRESH_RATE_UNSUPPORTED_FB = -1000101000, + XR_ERROR_COLOR_SPACE_UNSUPPORTED_FB = -1000108000, + XR_ERROR_UNEXPECTED_STATE_PASSTHROUGH_FB = -1000118000, + XR_ERROR_FEATURE_ALREADY_CREATED_PASSTHROUGH_FB = -1000118001, + XR_ERROR_FEATURE_REQUIRED_PASSTHROUGH_FB = -1000118002, + XR_ERROR_NOT_PERMITTED_PASSTHROUGH_FB = -1000118003, + XR_ERROR_INSUFFICIENT_RESOURCES_PASSTHROUGH_FB = -1000118004, + XR_ERROR_UNKNOWN_PASSTHROUGH_FB = -1000118050, + XR_ERROR_RENDER_MODEL_KEY_INVALID_FB = -1000119000, + XR_RENDER_MODEL_UNAVAILABLE_FB = 1000119020, + XR_ERROR_MARKER_NOT_TRACKED_VARJO = -1000124000, + XR_ERROR_MARKER_ID_INVALID_VARJO = -1000124001, + XR_ERROR_SPATIAL_ANCHOR_NAME_NOT_FOUND_MSFT = -1000142001, + XR_ERROR_SPATIAL_ANCHOR_NAME_INVALID_MSFT = -1000142002, + XR_RESULT_MAX_ENUM = 0x7FFFFFFF, + } + + /// + /// Values for type members of structs. + /// + /// + /// https://www.khronos.org/registry/OpenXR/specs/1.0/man/html/XrStructureType.html. + /// + internal enum XrStructureType + { + XR_TYPE_UNKNOWN = 0, + XR_TYPE_API_LAYER_PROPERTIES = 1, + XR_TYPE_EXTENSION_PROPERTIES = 2, + XR_TYPE_INSTANCE_CREATE_INFO = 3, + XR_TYPE_SYSTEM_GET_INFO = 4, + XR_TYPE_SYSTEM_PROPERTIES = 5, + XR_TYPE_VIEW_LOCATE_INFO = 6, + XR_TYPE_VIEW = 7, + XR_TYPE_SESSION_CREATE_INFO = 8, + XR_TYPE_SWAPCHAIN_CREATE_INFO = 9, + XR_TYPE_SESSION_BEGIN_INFO = 10, + XR_TYPE_VIEW_STATE = 11, + XR_TYPE_FRAME_END_INFO = 12, + XR_TYPE_HAPTIC_VIBRATION = 13, + XR_TYPE_EVENT_DATA_BUFFER = 16, + XR_TYPE_EVENT_DATA_INSTANCE_LOSS_PENDING = 17, + XR_TYPE_EVENT_DATA_SESSION_STATE_CHANGED = 18, + XR_TYPE_ACTION_STATE_BOOLEAN = 23, + XR_TYPE_ACTION_STATE_FLOAT = 24, + XR_TYPE_ACTION_STATE_VECTOR2F = 25, + XR_TYPE_ACTION_STATE_POSE = 27, + XR_TYPE_ACTION_SET_CREATE_INFO = 28, + XR_TYPE_ACTION_CREATE_INFO = 29, + XR_TYPE_INSTANCE_PROPERTIES = 32, + XR_TYPE_FRAME_WAIT_INFO = 33, + XR_TYPE_COMPOSITION_LAYER_PROJECTION = 35, + XR_TYPE_COMPOSITION_LAYER_QUAD = 36, + XR_TYPE_REFERENCE_SPACE_CREATE_INFO = 37, + XR_TYPE_ACTION_SPACE_CREATE_INFO = 38, + XR_TYPE_EVENT_DATA_REFERENCE_SPACE_CHANGE_PENDING = 40, + XR_TYPE_VIEW_CONFIGURATION_VIEW = 41, + XR_TYPE_SPACE_LOCATION = 42, + XR_TYPE_SPACE_VELOCITY = 43, + XR_TYPE_FRAME_STATE = 44, + XR_TYPE_VIEW_CONFIGURATION_PROPERTIES = 45, + XR_TYPE_FRAME_BEGIN_INFO = 46, + XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW = 48, + XR_TYPE_EVENT_DATA_EVENTS_LOST = 49, + XR_TYPE_INTERACTION_PROFILE_SUGGESTED_BINDING = 51, + XR_TYPE_EVENT_DATA_INTERACTION_PROFILE_CHANGED = 52, + XR_TYPE_INTERACTION_PROFILE_STATE = 53, + XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO = 55, + XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO = 56, + XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO = 57, + XR_TYPE_ACTION_STATE_GET_INFO = 58, + XR_TYPE_HAPTIC_ACTION_INFO = 59, + XR_TYPE_SESSION_ACTION_SETS_ATTACH_INFO = 60, + XR_TYPE_ACTIONS_SYNC_INFO = 61, + XR_TYPE_BOUND_SOURCES_FOR_ACTION_ENUMERATE_INFO = 62, + XR_TYPE_INPUT_SOURCE_LOCALIZED_NAME_GET_INFO = 63, + XR_TYPE_COMPOSITION_LAYER_CUBE_KHR = 1000006000, + XR_TYPE_INSTANCE_CREATE_INFO_ANDROID_KHR = 1000008000, + XR_TYPE_COMPOSITION_LAYER_DEPTH_INFO_KHR = 1000010000, + XR_TYPE_VULKAN_SWAPCHAIN_FORMAT_LIST_CREATE_INFO_KHR = 1000014000, + XR_TYPE_EVENT_DATA_PERF_SETTINGS_EXT = 1000015000, + XR_TYPE_COMPOSITION_LAYER_CYLINDER_KHR = 1000017000, + XR_TYPE_COMPOSITION_LAYER_EQUIRECT_KHR = 1000018000, + XR_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT = 1000019000, + XR_TYPE_DEBUG_UTILS_MESSENGER_CALLBACK_DATA_EXT = 1000019001, + XR_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT = 1000019002, + XR_TYPE_DEBUG_UTILS_LABEL_EXT = 1000019003, + XR_TYPE_GRAPHICS_BINDING_OPENGL_WIN32_KHR = 1000023000, + XR_TYPE_GRAPHICS_BINDING_OPENGL_XLIB_KHR = 1000023001, + XR_TYPE_GRAPHICS_BINDING_OPENGL_XCB_KHR = 1000023002, + XR_TYPE_GRAPHICS_BINDING_OPENGL_WAYLAND_KHR = 1000023003, + XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_KHR = 1000023004, + XR_TYPE_GRAPHICS_REQUIREMENTS_OPENGL_KHR = 1000023005, + XR_TYPE_GRAPHICS_BINDING_OPENGL_ES_ANDROID_KHR = 1000024001, + XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_ES_KHR = 1000024002, + XR_TYPE_GRAPHICS_REQUIREMENTS_OPENGL_ES_KHR = 1000024003, + XR_TYPE_GRAPHICS_BINDING_VULKAN_KHR = 1000025000, + XR_TYPE_SWAPCHAIN_IMAGE_VULKAN_KHR = 1000025001, + XR_TYPE_GRAPHICS_REQUIREMENTS_VULKAN_KHR = 1000025002, + XR_TYPE_GRAPHICS_BINDING_D3D11_KHR = 1000027000, + XR_TYPE_SWAPCHAIN_IMAGE_D3D11_KHR = 1000027001, + XR_TYPE_GRAPHICS_REQUIREMENTS_D3D11_KHR = 1000027002, + XR_TYPE_GRAPHICS_BINDING_D3D12_KHR = 1000028000, + XR_TYPE_SWAPCHAIN_IMAGE_D3D12_KHR = 1000028001, + XR_TYPE_GRAPHICS_REQUIREMENTS_D3D12_KHR = 1000028002, + XR_TYPE_SYSTEM_EYE_GAZE_INTERACTION_PROPERTIES_EXT = 1000030000, + XR_TYPE_EYE_GAZE_SAMPLE_TIME_EXT = 1000030001, + XR_TYPE_VISIBILITY_MASK_KHR = 1000031000, + XR_TYPE_EVENT_DATA_VISIBILITY_MASK_CHANGED_KHR = 1000031001, + XR_TYPE_SESSION_CREATE_INFO_OVERLAY_EXTX = 1000033000, + XR_TYPE_EVENT_DATA_MAIN_SESSION_VISIBILITY_CHANGED_EXTX = 1000033003, + XR_TYPE_COMPOSITION_LAYER_COLOR_SCALE_BIAS_KHR = 1000034000, + XR_TYPE_SPATIAL_ANCHOR_CREATE_INFO_MSFT = 1000039000, + XR_TYPE_SPATIAL_ANCHOR_SPACE_CREATE_INFO_MSFT = 1000039001, + XR_TYPE_COMPOSITION_LAYER_IMAGE_LAYOUT_FB = 1000040000, + XR_TYPE_COMPOSITION_LAYER_ALPHA_BLEND_FB = 1000041001, + XR_TYPE_VIEW_CONFIGURATION_DEPTH_RANGE_EXT = 1000046000, + XR_TYPE_GRAPHICS_BINDING_EGL_MNDX = 1000048004, + XR_TYPE_SPATIAL_GRAPH_NODE_SPACE_CREATE_INFO_MSFT = 1000049000, + XR_TYPE_SYSTEM_HAND_TRACKING_PROPERTIES_EXT = 1000051000, + XR_TYPE_HAND_TRACKER_CREATE_INFO_EXT = 1000051001, + XR_TYPE_HAND_JOINTS_LOCATE_INFO_EXT = 1000051002, + XR_TYPE_HAND_JOINT_LOCATIONS_EXT = 1000051003, + XR_TYPE_HAND_JOINT_VELOCITIES_EXT = 1000051004, + XR_TYPE_SYSTEM_HAND_TRACKING_MESH_PROPERTIES_MSFT = 1000052000, + XR_TYPE_HAND_MESH_SPACE_CREATE_INFO_MSFT = 1000052001, + XR_TYPE_HAND_MESH_UPDATE_INFO_MSFT = 1000052002, + XR_TYPE_HAND_MESH_MSFT = 1000052003, + XR_TYPE_HAND_POSE_TYPE_INFO_MSFT = 1000052004, + XR_TYPE_SECONDARY_VIEW_CONFIGURATION_SESSION_BEGIN_INFO_MSFT = 1000053000, + XR_TYPE_SECONDARY_VIEW_CONFIGURATION_STATE_MSFT = 1000053001, + XR_TYPE_SECONDARY_VIEW_CONFIGURATION_FRAME_STATE_MSFT = 1000053002, + XR_TYPE_SECONDARY_VIEW_CONFIGURATION_FRAME_END_INFO_MSFT = 1000053003, + XR_TYPE_SECONDARY_VIEW_CONFIGURATION_LAYER_INFO_MSFT = 1000053004, + XR_TYPE_SECONDARY_VIEW_CONFIGURATION_SWAPCHAIN_CREATE_INFO_MSFT = 1000053005, + XR_TYPE_CONTROLLER_MODEL_KEY_STATE_MSFT = 1000055000, + XR_TYPE_CONTROLLER_MODEL_NODE_PROPERTIES_MSFT = 1000055001, + XR_TYPE_CONTROLLER_MODEL_PROPERTIES_MSFT = 1000055002, + XR_TYPE_CONTROLLER_MODEL_NODE_STATE_MSFT = 1000055003, + XR_TYPE_CONTROLLER_MODEL_STATE_MSFT = 1000055004, + XR_TYPE_VIEW_CONFIGURATION_VIEW_FOV_EPIC = 1000059000, + XR_TYPE_HOLOGRAPHIC_WINDOW_ATTACHMENT_MSFT = 1000063000, + XR_TYPE_COMPOSITION_LAYER_REPROJECTION_INFO_MSFT = 1000066000, + XR_TYPE_COMPOSITION_LAYER_REPROJECTION_PLANE_OVERRIDE_MSFT = 1000066001, + XR_TYPE_ANDROID_SURFACE_SWAPCHAIN_CREATE_INFO_FB = 1000070000, + XR_TYPE_COMPOSITION_LAYER_SECURE_CONTENT_FB = 1000072000, + XR_TYPE_INTERACTION_PROFILE_ANALOG_THRESHOLD_VALVE = 1000079000, + XR_TYPE_HAND_JOINTS_MOTION_RANGE_INFO_EXT = 1000080000, + XR_TYPE_LOADER_INIT_INFO_ANDROID_KHR = 1000089000, + XR_TYPE_VULKAN_INSTANCE_CREATE_INFO_KHR = 1000090000, + XR_TYPE_VULKAN_DEVICE_CREATE_INFO_KHR = 1000090001, + XR_TYPE_VULKAN_GRAPHICS_DEVICE_GET_INFO_KHR = 1000090003, + XR_TYPE_COMPOSITION_LAYER_EQUIRECT2_KHR = 1000091000, + XR_TYPE_SCENE_OBSERVER_CREATE_INFO_MSFT = 1000097000, + XR_TYPE_SCENE_CREATE_INFO_MSFT = 1000097001, + XR_TYPE_NEW_SCENE_COMPUTE_INFO_MSFT = 1000097002, + XR_TYPE_VISUAL_MESH_COMPUTE_LOD_INFO_MSFT = 1000097003, + XR_TYPE_SCENE_COMPONENTS_MSFT = 1000097004, + XR_TYPE_SCENE_COMPONENTS_GET_INFO_MSFT = 1000097005, + XR_TYPE_SCENE_COMPONENT_LOCATIONS_MSFT = 1000097006, + XR_TYPE_SCENE_COMPONENTS_LOCATE_INFO_MSFT = 1000097007, + XR_TYPE_SCENE_OBJECTS_MSFT = 1000097008, + XR_TYPE_SCENE_COMPONENT_PARENT_FILTER_INFO_MSFT = 1000097009, + XR_TYPE_SCENE_OBJECT_TYPES_FILTER_INFO_MSFT = 1000097010, + XR_TYPE_SCENE_PLANES_MSFT = 1000097011, + XR_TYPE_SCENE_PLANE_ALIGNMENT_FILTER_INFO_MSFT = 1000097012, + XR_TYPE_SCENE_MESHES_MSFT = 1000097013, + XR_TYPE_SCENE_MESH_BUFFERS_GET_INFO_MSFT = 1000097014, + XR_TYPE_SCENE_MESH_BUFFERS_MSFT = 1000097015, + XR_TYPE_SCENE_MESH_VERTEX_BUFFER_MSFT = 1000097016, + XR_TYPE_SCENE_MESH_INDICES_UINT32_MSFT = 1000097017, + XR_TYPE_SCENE_MESH_INDICES_UINT16_MSFT = 1000097018, + XR_TYPE_SERIALIZED_SCENE_FRAGMENT_DATA_GET_INFO_MSFT = 1000098000, + XR_TYPE_SCENE_DESERIALIZE_INFO_MSFT = 1000098001, + XR_TYPE_EVENT_DATA_DISPLAY_REFRESH_RATE_CHANGED_FB = 1000101000, + XR_TYPE_VIVE_TRACKER_PATHS_HTCX = 1000103000, + XR_TYPE_EVENT_DATA_VIVE_TRACKER_CONNECTED_HTCX = 1000103001, + XR_TYPE_SYSTEM_FACIAL_TRACKING_PROPERTIES_HTC = 1000104000, + XR_TYPE_FACIAL_TRACKER_CREATE_INFO_HTC = 1000104001, + XR_TYPE_FACIAL_EXPRESSIONS_HTC = 1000104002, + XR_TYPE_SYSTEM_COLOR_SPACE_PROPERTIES_FB = 1000108000, + XR_TYPE_HAND_TRACKING_MESH_FB = 1000110001, + XR_TYPE_HAND_TRACKING_SCALE_FB = 1000110003, + XR_TYPE_HAND_TRACKING_AIM_STATE_FB = 1000111001, + XR_TYPE_HAND_TRACKING_CAPSULES_STATE_FB = 1000112000, + XR_TYPE_FOVEATION_PROFILE_CREATE_INFO_FB = 1000114000, + XR_TYPE_SWAPCHAIN_CREATE_INFO_FOVEATION_FB = 1000114001, + XR_TYPE_SWAPCHAIN_STATE_FOVEATION_FB = 1000114002, + XR_TYPE_FOVEATION_LEVEL_PROFILE_CREATE_INFO_FB = 1000115000, + XR_TYPE_KEYBOARD_SPACE_CREATE_INFO_FB = 1000116009, + XR_TYPE_KEYBOARD_TRACKING_QUERY_FB = 1000116004, + XR_TYPE_SYSTEM_KEYBOARD_TRACKING_PROPERTIES_FB = 1000116002, + XR_TYPE_TRIANGLE_MESH_CREATE_INFO_FB = 1000117001, + XR_TYPE_SYSTEM_PASSTHROUGH_PROPERTIES_FB = 1000118000, + XR_TYPE_PASSTHROUGH_CREATE_INFO_FB = 1000118001, + XR_TYPE_PASSTHROUGH_LAYER_CREATE_INFO_FB = 1000118002, + XR_TYPE_COMPOSITION_LAYER_PASSTHROUGH_FB = 1000118003, + XR_TYPE_GEOMETRY_INSTANCE_CREATE_INFO_FB = 1000118004, + XR_TYPE_GEOMETRY_INSTANCE_TRANSFORM_FB = 1000118005, + XR_TYPE_PASSTHROUGH_STYLE_FB = 1000118020, + XR_TYPE_PASSTHROUGH_COLOR_MAP_MONO_TO_RGBA_FB = 1000118021, + XR_TYPE_PASSTHROUGH_COLOR_MAP_MONO_TO_MONO_FB = 1000118022, + XR_TYPE_EVENT_DATA_PASSTHROUGH_STATE_CHANGED_FB = 1000118030, + XR_TYPE_RENDER_MODEL_PATH_INFO_FB = 1000119000, + XR_TYPE_RENDER_MODEL_PROPERTIES_FB = 1000119001, + XR_TYPE_RENDER_MODEL_BUFFER_FB = 1000119002, + XR_TYPE_RENDER_MODEL_LOAD_INFO_FB = 1000119003, + XR_TYPE_SYSTEM_RENDER_MODEL_PROPERTIES_FB = 1000119004, + XR_TYPE_BINDING_MODIFICATIONS_KHR = 1000120000, + XR_TYPE_VIEW_LOCATE_FOVEATED_RENDERING_VARJO = 1000121000, + XR_TYPE_FOVEATED_VIEW_CONFIGURATION_VIEW_VARJO = 1000121001, + XR_TYPE_SYSTEM_FOVEATED_RENDERING_PROPERTIES_VARJO = 1000121002, + XR_TYPE_COMPOSITION_LAYER_DEPTH_TEST_VARJO = 1000122000, + XR_TYPE_SYSTEM_MARKER_TRACKING_PROPERTIES_VARJO = 1000124000, + XR_TYPE_EVENT_DATA_MARKER_TRACKING_UPDATE_VARJO = 1000124001, + XR_TYPE_MARKER_SPACE_CREATE_INFO_VARJO = 1000124002, + XR_TYPE_SPATIAL_ANCHOR_PERSISTENCE_INFO_MSFT = 1000142000, + XR_TYPE_SPATIAL_ANCHOR_FROM_PERSISTED_ANCHOR_CREATE_INFO_MSFT = 1000142001, + XR_TYPE_SWAPCHAIN_IMAGE_FOVEATION_VULKAN_FB = 1000160000, + XR_TYPE_SWAPCHAIN_STATE_ANDROID_SURFACE_DIMENSIONS_FB = 1000161000, + XR_TYPE_SWAPCHAIN_STATE_SAMPLER_OPENGL_ES_FB = 1000162000, + XR_TYPE_SWAPCHAIN_STATE_SAMPLER_VULKAN_FB = 1000163000, + XR_TYPE_COMPOSITION_LAYER_SPACE_WARP_INFO_FB = 1000171000, + XR_TYPE_SYSTEM_SPACE_WARP_PROPERTIES_FB = 1000171001, + XR_TYPE_DIGITAL_LENS_CONTROL_ALMALENCE = 1000196000, + XR_TYPE_PASSTHROUGH_KEYBOARD_HANDS_INTENSITY_FB = 1000203002, + XR_TYPE_GRAPHICS_BINDING_VULKAN2_KHR = XR_TYPE_GRAPHICS_BINDING_VULKAN_KHR, + XR_TYPE_SWAPCHAIN_IMAGE_VULKAN2_KHR = XR_TYPE_SWAPCHAIN_IMAGE_VULKAN_KHR, + XR_TYPE_GRAPHICS_REQUIREMENTS_VULKAN2_KHR = XR_TYPE_GRAPHICS_REQUIREMENTS_VULKAN_KHR, + XR_STRUCTURE_TYPE_MAX_ENUM = 0x7FFFFFFF, + } + + /// + /// Values for types of actions. + /// + /// + /// https://www.khronos.org/registry/OpenXR/specs/1.0/man/html/XrActionType.html. + /// + internal enum XrActionType + { + XR_ACTION_TYPE_BOOLEAN_INPUT = 1, + XR_ACTION_TYPE_FLOAT_INPUT = 2, + XR_ACTION_TYPE_VECTOR2F_INPUT = 3, + XR_ACTION_TYPE_POSE_INPUT = 4, + XR_ACTION_TYPE_VIBRATION_OUTPUT = 100, + XR_ACTION_TYPE_MAX_ENUM = 0x7FFFFFFF, + } + +#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value + /// + /// Information to create a hand joints handle. + /// + /// + /// https://www.khronos.org/registry/OpenXR/specs/1.0/man/html/XrHandTrackerCreateInfoEXT.html. + /// + internal struct XrHandTrackerCreateInfoEXT + { +#pragma warning disable SA1600 // Elements should be documented + public XrStructureType Type; + public IntPtr Next; + public XrHandEXT Hand; + public XrHandJointSetEXT HandJointSet; +#pragma warning restore SA1600 // Elements should be documented + } + + /// + /// Information to create an action set. + /// + /// + /// https://www.khronos.org/registry/OpenXR/specs/1.0/man/html/XrActionSetCreateInfo.html. + /// + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] + internal struct XrActionSetCreateInfo + { +#pragma warning disable SA1600 // Elements should be documented + public XrStructureType Type; + public IntPtr Next; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)] + public string ActionSetName; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] + public string LocalizedActionSetName; + public uint Priority; +#pragma warning restore SA1600 // Elements should be documented + } + + /// + /// Information to create an action. + /// + /// + /// https://www.khronos.org/registry/OpenXR/specs/1.0/man/html/XrActionCreateInfo.html. + /// + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] + internal struct XrActionCreateInfo + { +#pragma warning disable SA1600 // Elements should be documented + public XrStructureType Type; + public IntPtr Next; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)] + public string ActionName; + public XrActionType ActionType; + public uint CountSubactionPaths; + public IntPtr SubactionPaths; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] + public string LocalizedActionName; +#pragma warning restore SA1600 // Elements should be documented + } + + /// + /// Suggested binding for a single action. + /// + /// + /// https://www.khronos.org/registry/OpenXR/specs/1.0/man/html/XrActionSuggestedBinding.html. + /// + internal struct XrActionSuggestedBinding + { +#pragma warning disable SA1600 // Elements should be documented + public ulong Action; + public ulong Binding; +#pragma warning restore SA1600 // Elements should be documented + } + + /// + /// Suggested bindings for an interaction profile. + /// + /// + /// https://www.khronos.org/registry/OpenXR/specs/1.0/man/html/XrInteractionProfileSuggestedBinding.html. + /// + internal struct XrInteractionProfileSuggestedBinding + { +#pragma warning disable SA1600 // Elements should be documented + public XrStructureType Type; + public IntPtr Next; + public ulong InteractionProfile; + public uint CountSuggestedBindings; + public IntPtr SuggestedBindings; +#pragma warning restore SA1600 // Elements should be documented + } + + /// + /// Information to attach action sets to a session. + /// + /// + /// https://www.khronos.org/registry/OpenXR/specs/1.0/man/html/XrSessionActionSetsAttachInfo.html. + /// + internal struct XrSessionActionSetsAttachInfo + { +#pragma warning disable SA1600 // Elements should be documented + public XrStructureType Type; + public IntPtr Next; + public uint CountActionSets; + public IntPtr ActionSets; +#pragma warning restore SA1600 // Elements should be documented + } + + /// + /// Creation info for an action space. + /// + /// + /// https://www.khronos.org/registry/OpenXR/specs/1.0/man/html/XrActionSpaceCreateInfo.html. + /// + internal struct XrActionSpaceCreateInfo + { +#pragma warning disable SA1600 // Elements should be documented + public XrStructureType Type; + public IntPtr Next; + public ulong Action; + public ulong SubactionPath; + public XrPosef PoseInActionSpace; +#pragma warning restore SA1600 // Elements should be documented + } + + /// + /// Describes an active action set. + /// + /// + /// https://www.khronos.org/registry/OpenXR/specs/1.0/man/html/XrActiveActionSet.html. + /// + internal struct XrActiveActionSet + { +#pragma warning disable SA1600 // Elements should be documented + public ulong ActionSet; + public ulong SubactionPath; +#pragma warning restore SA1600 // Elements should be documented + } + + /// + /// Information to sync actions. + /// + /// + /// https://www.khronos.org/registry/OpenXR/specs/1.0/man/html/XrActionsSyncInfo.html. + /// + internal struct XrActionsSyncInfo + { +#pragma warning disable SA1600 // Elements should be documented + public XrStructureType Type; + public IntPtr Next; + public uint CountActiveActionSets; + public IntPtr ActiveActionSets; +#pragma warning restore SA1600 // Elements should be documented + } + + /// + /// Pose action metadata. + /// + /// + /// https://www.khronos.org/registry/OpenXR/specs/1.0/man/html/XrActionStatePose.html. + /// + internal struct XrActionStatePose + { +#pragma warning disable SA1600 // Elements should be documented + public XrStructureType Type; + public IntPtr Next; + public bool IsActive; +#pragma warning restore SA1600 // Elements should be documented + } + + /// + /// Information to get action state. + /// + /// + /// https://www.khronos.org/registry/OpenXR/specs/1.0/man/html/XrActionStateGetInfo.html. + /// + internal struct XrActionStateGetInfo + { +#pragma warning disable SA1600 // Elements should be documented + public XrStructureType Type; + public IntPtr Next; + public ulong Action; + public ulong SubactionPath; +#pragma warning restore SA1600 // Elements should be documented + } + + /// + /// Contains info about a space. + /// + /// + /// https://www.khronos.org/registry/OpenXR/specs/1.0/man/html/XrSpaceLocation.html. + /// + internal struct XrSpaceLocation + { +#pragma warning disable SA1600 // Elements should be documented + public XrStructureType Type; + public IntPtr Next; + public ulong LocationFlags; + public XrPosef Pose; +#pragma warning restore SA1600 // Elements should be documented + } + + /// + /// Eye gaze sample time structure. + /// + /// + /// https://www.khronos.org/registry/OpenXR/specs/1.0/man/html/XrEyeGazeSampleTimeEXT.html. + /// + internal struct XrEyeGazeSampleTimeEXT + { +#pragma warning disable SA1600 // Elements should be documented + public XrStructureType Type; + public IntPtr Next; + public long Time; +#pragma warning restore SA1600 // Elements should be documented + } + + /// + /// Describes the information to locate hand joints. + /// + /// + /// https://www.khronos.org/registry/OpenXR/specs/1.0/man/html/XrHandJointsLocateInfoEXT.html. + /// + internal struct XrHandJointsLocateInfoEXT + { +#pragma warning disable SA1600 // Elements should be documented + public XrStructureType Type; + public IntPtr Next; + public ulong BaseSpace; + public long Time; +#pragma warning restore SA1600 // Elements should be documented + } + + /// + /// Returns the hand joint locations. + /// + /// + /// https://www.khronos.org/registry/OpenXR/specs/1.0/man/html/XrHandJointLocationsEXT.html. + /// + internal struct XrHandJointLocationsEXT + { +#pragma warning disable SA1600 // Elements should be documented + public XrStructureType Type; + public IntPtr Next; + public bool IsActive; + public uint JointCount; + public IntPtr JointLocations; +#pragma warning restore SA1600 // Elements should be documented + } + + /// + /// Describes the location and radius of a hand joint. + /// + /// + /// https://www.khronos.org/registry/OpenXR/specs/1.0/man/html/XrHandJointLocationEXT.html. + /// + internal struct XrHandJointLocationEXT + { +#pragma warning disable SA1600 // Elements should be documented + public ulong LocationFlags; + public XrPosef Pose; + public float Radius; +#pragma warning restore SA1600 // Elements should be documented + } + + /// + /// Location and orientation in a space. + /// + /// + /// https://www.khronos.org/registry/OpenXR/specs/1.0/man/html/XrPosef.html + /// We use the StereoKit and types for convenience + /// because they have the necessary structure already. + /// + internal struct XrPosef + { +#pragma warning disable SA1600 // Elements should be documented + public Quat Orientation; + public Vec3 Position; +#pragma warning restore SA1600 // Elements should be documented + } +#pragma warning restore CS0649 // Field is never assigned to, and will always have its default value + + /// + /// Space location flags. + /// + /// + /// https://www.khronos.org/registry/OpenXR/specs/1.0/man/html/XrSpaceLocationFlags.html. + /// + internal static class XrSpaceLocationFlags + { + /// + /// Indicates that the pose field's orientation field contains valid data. + /// For a space location tracking a device with its own inertial tracking, + /// should remain set when this bit is set. Applications must not read the pose field's orientation if this flag is unset. + /// + internal const uint XR_SPACE_LOCATION_ORIENTATION_VALID_BIT = 0x00000001; + + /// + /// indicates that the pose field's position field contains valid data. + /// When a space location loses tracking, runtimes should continue to provide valid but untracked position values that are + /// inferred or last-known, so long as it's still meaningful for the application to use that position, clearing + /// until positional tracking is recovered. + /// Applications must not read the pose field's position if this flag is unset. + /// + internal const uint XR_SPACE_LOCATION_POSITION_VALID_BIT = 0x00000002; + + /// + /// indicates that the pose field's orientation field represents + /// an actively tracked orientation. For a space location tracking a device with its own inertial tracking, this bit should + /// remain set when is set. For a space location tracking an object + /// whose orientation is no longer known during tracking loss (e.g. an observed QR code), runtimes should continue to provide + /// valid but untracked orientation values, so long as it's still meaningful for the application to use that orientation. + /// + internal const uint XR_SPACE_LOCATION_ORIENTATION_TRACKED_BIT = 0x00000004; + + /// + /// indicates that the pose field's position field represents an actively + /// tracked position. When a space location loses tracking, runtimes should continue to provide valid but untracked position + /// values that are inferred or last-known, e.g. based on neck model updates, inertial dead reckoning, or a last-known position, + /// so long as it's still meaningful for the application to use that position. + /// + internal const uint XR_SPACE_LOCATION_POSITION_TRACKED_BIT = 0x00000008; + } +} +#pragma warning restore SA1310 // Field names should not contain underscore +#pragma warning restore SA1602 // Enumeration items should be documented +#pragma warning restore SA1649 // File name should match first type name \ No newline at end of file diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality/OpenXR/Operators.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality/OpenXR/Operators.cs new file mode 100644 index 000000000..bb4cf9f9e --- /dev/null +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality/OpenXR/Operators.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.Psi.MixedReality.OpenXR +{ + using System; + using MathNet.Spatial.Euclidean; + + /// + /// Implements operators. + /// + public static partial class Operators + { + /// + /// Interpolates between two poses by interpolating the + /// poses of each joint. Spherical linear interpolation () + /// is used for rotation, and linear interpolation is used for translation. + /// + /// The first pose. + /// The second pose. + /// The amount to interpolate between the two hand poses. + /// A value between 0 and 1 will return an interpolation between the two values. + /// A value outside the 0-1 range will generate an extrapolated result. + /// The interpolated pose. + /// Returns null if either input hand is null. + public static Hand InterpolateHands(Hand hand1, Hand hand2, double amount) + { + if (hand1 is null || hand2 is null) + { + return null; + } + + // Interpolate each joint pose separately + int numJoints = (int)HandJointIndex.MaxIndex; + var interpolatedJoints = new CoordinateSystem[numJoints]; + for (int i = 0; i < numJoints; i++) + { + interpolatedJoints[i] = Spatial.Euclidean.Operators.InterpolateCoordinateSystems(hand1.Joints[i], hand2.Joints[i], amount); + } + + // Use values from hand2 for boolean members if the amount we are + // interpolating is greater than 0.5. Otherwise select from hand1. + bool[] interpolatedJointsValid = null; + bool[] interpolatedJointsTracked = null; + + if (hand1.JointsValid is not null && hand2.JointsValid is not null) + { + interpolatedJointsValid = new bool[numJoints]; + Array.Copy(amount > 0.5 ? hand2.JointsValid : hand1.JointsValid, interpolatedJointsValid, numJoints); + } + + if (hand1.JointsTracked is not null && hand2.JointsTracked is not null) + { + interpolatedJointsTracked = new bool[numJoints]; + Array.Copy(amount > 0.5 ? hand2.JointsTracked : hand1.JointsTracked, interpolatedJointsTracked, numJoints); + } + + return new Hand( + amount > 0.5 ? hand2.IsActive : hand1.IsActive, + interpolatedJoints, + interpolatedJointsValid, + interpolatedJointsTracked); + } + } +} diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality/Operators.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality/Operators.cs index ff5a325cf..2a02a3b50 100644 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality/Operators.cs +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality/Operators.cs @@ -7,11 +7,7 @@ namespace Microsoft.Psi.MixedReality using System.Numerics; using MathNet.Spatial.Euclidean; using Microsoft.Psi.Spatial.Euclidean; - using StereoKit; using static Microsoft.Psi.Spatial.Euclidean.Operators; - using StereoKitColor = StereoKit.Color; - using StereoKitColor32 = StereoKit.Color32; - using SystemDrawingColor = System.Drawing.Color; /// /// Implements operators. @@ -21,128 +17,6 @@ public static partial class Operators private static readonly CoordinateSystem HoloLensBasis = new (default, UnitVector3D.ZAxis.Negate(), UnitVector3D.XAxis.Negate(), UnitVector3D.YAxis); private static readonly CoordinateSystem HoloLensBasisInverted = HoloLensBasis.Invert(); - /// - /// Converts a to a , - /// changing basis from HoloLens to MathNet. - /// - /// The to be converted. - /// The . - /// - /// The HoloLens basis assumes that Forward=-Z, Left=-X, and Up=Y. - /// The MathNet basis assumes that Forward=X, Left=Y, and Up=Z. - /// - public static CoordinateSystem ToCoordinateSystem(this StereoKit.Matrix stereoKitMatrix) - { - Matrix4x4 systemMatrix = stereoKitMatrix; - return systemMatrix.RebaseToMathNetCoordinateSystem(); - } - - /// - /// Converts a StereoKit to a , - /// changing basis from HoloLens to MathNet. - /// - /// The StereoKit to be converted. - /// The . - /// - /// The HoloLens basis assumes that Forward=-Z, Left=-X, and Up=Y. - /// The MathNet basis assumes that Forward=X, Left=Y, and Up=Z. - /// - public static CoordinateSystem ToCoordinateSystem(this Pose pose) - { - return pose.ToMatrix().ToCoordinateSystem(); - } - - /// - /// Converts a to a , - /// changing basis from MathNet to HoloLens. - /// - /// The to be converted. - /// The . - /// - /// The HoloLens basis assumes that Forward=-Z, Left=-X, and Up=Y. - /// The MathNet basis assumes that Forward=X, Left=Y, and Up=Z. - /// - public static StereoKit.Matrix ToStereoKitMatrix(this CoordinateSystem coordinateSystem) - { - return new StereoKit.Matrix(coordinateSystem.RebaseToHoloLensSystemMatrix()); - } - - /// - /// Converts a pose to a StereoKit , - /// changing basis from MathNet to HoloLens. - /// - /// The pose to be converted. - /// The . - /// - /// The HoloLens basis assumes that Forward=-Z, Left=-X, and Up=Y. - /// The MathNet basis assumes that Forward=X, Left=Y, and Up=Z. - /// - public static Pose ToStereoKitPose(this CoordinateSystem coordinateSystem) - { - return coordinateSystem.ToStereoKitMatrix().Pose; - } - - /// - /// Convert to , changing the basis from MathNet to HoloLens. - /// - /// to be converted. - /// . - /// - /// The HoloLens basis assumes that Forward=-Z, Left=-X, and Up=Y. - /// The MathNet basis assumes that Forward=X, Left=Y, and Up=Z. - /// - public static Vec3 ToVec3(this Point3D point3d) - { - // Change of basis happening in place here. - return new Vec3(-(float)point3d.Y, (float)point3d.Z, -(float)point3d.X); - } - - /// - /// Convert to , changing the basis from HoloLens to MathNet. - /// - /// to be converted. - /// . - /// - /// The HoloLens basis assumes that Forward=-Z, Left=-X, and Up=Y. - /// The MathNet basis assumes that Forward=X, Left=Y, and Up=Z. - /// - public static Point3D ToPoint3D(this Vec3 vec3) - { - // Change of basis happening in place here. - return new Point3D(-vec3.z, -vec3.x, vec3.y); - } - - /// - /// Convert to , changing the basis from HoloLens to MathNet. - /// - /// to be converted. - /// . - /// - /// The HoloLens basis assumes that Forward=-Z, Left=-X, and Up=Y. - /// The MathNet basis assumes that Forward=X, Left=Y, and Up=Z. - /// - public static Point3D ToPoint3D(this Vector3 vector3) - { - Vec3 v = vector3; - return v.ToPoint3D(); - } - - /// - /// Converts a specified to a . - /// - /// The . - /// The corresponding . - public static StereoKitColor ToStereoKitColor(this SystemDrawingColor color) - => new ((float)color.R / 255, (float)color.G / 255, (float)color.B / 255, (float)color.A / 255); - - /// - /// Converts a specified to a . - /// - /// The . - /// The corresponding . - public static StereoKitColor32 ToStereoKitColor32(this SystemDrawingColor color) - => new (color.R, color.G, color.B, color.A); - /// /// Convert stream of frames of IMU samples to flattened stream of samples within. /// @@ -165,16 +39,6 @@ public static StereoKitColor32 ToStereoKitColor32(this SystemDrawingColor color) deliveryPolicy, name); - /// - /// Gets the current StereoKit frame time from OpenXR as a \psi pipeline . - /// - /// The pipeline to get the current time for. - /// The current frame time. - public static DateTime GetCurrentTimeFromOpenXr(this Pipeline pipeline) - { - return pipeline.ConvertTimeFromOpenXr(Backend.OpenXR.Time); - } - /// /// Converts a time from OpenXR into the equivalent \psi pipeline . /// @@ -188,40 +52,28 @@ public static DateTime ConvertTimeFromOpenXr(this Pipeline pipeline, long openXr } /// - /// Interpolates between two poses by interpolating the - /// poses of each joint. Spherical linear interpolation () - /// is used for rotation, and linear interpolation is used for translation. + /// Convert a to a , changing the basis from HoloLens to MathNet. /// - /// The first pose. - /// The second pose. - /// The amount to interpolate between the two hand poses. A value of 0 will - /// effectively return the first hand pose, a value of 1 will effectively return the second - /// hand pose, and a value between 0 and 1 will return an interpolation between those two values. - /// A value outside the 0-1 range will generate an extrapolated result. - /// The interpolated pose. - public static Hand InterpolateHands(Hand hand1, Hand hand2, double amount) - { - int numJoints = (int)HandJointIndex.MaxIndex; - - // Initialize a default pose if either input hand is null - hand1 ??= new Hand(false, false, false, new CoordinateSystem[numJoints]); - hand2 ??= new Hand(false, false, false, new CoordinateSystem[numJoints]); - - // Interpolate each joint pose separately - var interpolatedJoints = new CoordinateSystem[numJoints]; - for (int i = 0; i < numJoints; i++) - { - interpolatedJoints[i] = InterpolateCoordinateSystems(hand1.Joints[i], hand2.Joints[i], amount); - } + /// The to be converted. + /// . + /// + /// The HoloLens basis assumes that Forward=-Z, Left=-X, and Up=Y. + /// The MathNet basis assumes that Forward=X, Left=Y, and Up=Z. + /// + public static Point3D ToPoint3D(this Vector3 vector3) + => new (-vector3.Z, -vector3.X, vector3.Y); - // Use values from hand2 for tracked, pinched, and gripped, if the amount we are interpolating - // is greater than 0.5. Otherwise select from hand1. - return new Hand( - amount > 0.5 ? hand2.IsTracked : hand1.IsTracked, - amount > 0.5 ? hand2.IsPinched : hand1.IsPinched, - amount > 0.5 ? hand2.IsGripped : hand1.IsGripped, - interpolatedJoints); - } + /// + /// Convert a to a , changing the basis from HoloLens to MathNet. + /// + /// The to be converted. + /// . + /// + /// The HoloLens basis assumes that Forward=-Z, Left=-X, and Up=Y. + /// The MathNet basis assumes that Forward=X, Left=Y, and Up=Z. + /// + public static Vector3D ToVector3D(this Vector3 vector3) + => new (-vector3.Z, -vector3.X, vector3.Y); /// /// Converts and rebases a MathNet to a HoloLens . diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality/EyesSensor.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/EyesSensor.cs similarity index 80% rename from Sources/MixedReality/Microsoft.Psi.MixedReality/EyesSensor.cs rename to Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/EyesSensor.cs index db8cdac94..3104be7db 100644 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality/EyesSensor.cs +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/EyesSensor.cs @@ -1,12 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -namespace Microsoft.Psi.MixedReality +namespace Microsoft.Psi.MixedReality.StereoKit { using System; + using global::StereoKit; using MathNet.Spatial.Euclidean; using Microsoft.Psi.Components; - using StereoKit; /// /// Source component that surfaces eye tracking information on a stream. @@ -61,13 +61,16 @@ public void Stop(DateTime finalOriginatingTime, Action notifyCompleted) /// public override void Step() { - // Get the current time from OpenXR - var currentSampleTime = this.pipeline.GetCurrentTimeFromOpenXr(); - - if (this.active && currentSampleTime - this.Out.LastEnvelope.OriginatingTime >= this.interval) + if (this.active) { - this.Out.Post(PsiInput.Eyes, currentSampleTime); - this.EyesTracked.Post(Input.EyesTracked.IsActive(), currentSampleTime); + var currentSampleTime = this.pipeline.ConvertTimeFromOpenXr(Backend.OpenXR.EyesSampleTime); + var elapsedTime = currentSampleTime - this.Out.LastEnvelope.OriginatingTime; + + if (elapsedTime.Ticks > 0 && elapsedTime >= this.interval) + { + this.Out.Post(PsiInput.Eyes, currentSampleTime); + this.EyesTracked.Post(Input.EyesTracked.IsActive(), currentSampleTime); + } } } } diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality/Hand.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/Hand.cs similarity index 90% rename from Sources/MixedReality/Microsoft.Psi.MixedReality/Hand.cs rename to Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/Hand.cs index 25b3ea1a0..5259f15da 100644 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality/Hand.cs +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/Hand.cs @@ -1,13 +1,13 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -namespace Microsoft.Psi.MixedReality +namespace Microsoft.Psi.MixedReality.StereoKit { using System.Collections.Generic; using MathNet.Spatial.Euclidean; /// - /// Represents one of the user's hands. + /// Represents one of the user's hands, as produced by the StereoKit-based component. /// public class Hand { @@ -26,6 +26,15 @@ public Hand(bool isTracked, bool isPinched, bool isGripped, CoordinateSystem[] j this.Joints = joints; } + /// + /// Gets an empty Hand instance. + /// + public static Hand Empty => new ( + false, + false, + false, + new CoordinateSystem[(int)HandJointIndex.MaxIndex]); + /// /// Gets the definition of bones connecting the hand joints. /// diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality/Handle.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/Handle.cs similarity index 90% rename from Sources/MixedReality/Microsoft.Psi.MixedReality/Handle.cs rename to Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/Handle.cs index a5f20ce40..f5f55b308 100644 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality/Handle.cs +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/Handle.cs @@ -1,12 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -namespace Microsoft.Psi.MixedReality +namespace Microsoft.Psi.MixedReality.StereoKit { using System; + using global::StereoKit; using MathNet.Spatial.Euclidean; using Microsoft.Psi.Components; - using StereoKit; /// /// Component that represents a movable UI handle. @@ -59,13 +59,19 @@ public void Stop(DateTime finalOriginatingTime, Action notifyCompleted) } /// - protected override void Render() + public override void Step() { if (this.active) { - UI.Handle(this.id, ref this.pose, this.bounds, this.show); + base.Step(); this.Out.Post(this.pose.ToCoordinateSystem(), this.pipeline.GetCurrentTime()); } } + + /// + protected override void Render() + { + UI.Handle(this.id, ref this.pose, this.bounds, this.show); + } } } diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality/HandsSensor.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/HandsSensor.cs similarity index 74% rename from Sources/MixedReality/Microsoft.Psi.MixedReality/HandsSensor.cs rename to Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/HandsSensor.cs index 4c837339d..83e5d3916 100644 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality/HandsSensor.cs +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/HandsSensor.cs @@ -1,26 +1,28 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -namespace Microsoft.Psi.MixedReality +namespace Microsoft.Psi.MixedReality.StereoKit { using System; using Microsoft.Psi.Components; /// - /// Source component that produces streams containing information about the tracked hands. + /// Source component that produces streams containing information about the tracked hands (via StereoKit). /// public class HandsSensor : StereoKitComponent, ISourceComponent, IProducer<(Hand Left, Hand Right)> { private readonly Pipeline pipeline; private readonly TimeSpan interval; + private Emitter leftHandEmitter = null; + private Emitter rightHandEmitter = null; private bool active; /// /// Initializes a new instance of the class. /// /// The pipeline to add the component to. - /// Optional interval at which to poll hand information (default 1/60th second). + /// Optional interval at which to poll hand information (default to frequency of StereoKit Stepper). /// An optional name for the component. public HandsSensor(Pipeline pipeline, TimeSpan interval = default, string name = nameof(HandsSensor)) : base(pipeline, name) @@ -29,8 +31,6 @@ public HandsSensor(Pipeline pipeline, TimeSpan interval = default, string name = this.interval = interval == default ? TimeSpan.FromTicks(1) : interval; // minimum interval of one-tick this.Out = pipeline.CreateEmitter<(Hand Left, Hand Right)>(this, nameof(this.Out)); - this.Left = pipeline.CreateEmitter(this, nameof(this.Left)); - this.Right = pipeline.CreateEmitter(this, nameof(this.Right)); } /// @@ -39,12 +39,18 @@ public HandsSensor(Pipeline pipeline, TimeSpan interval = default, string name = /// /// Gets the stream of left hand information. /// - public Emitter Left { get; } + public Emitter Left => this.leftHandEmitter ??= this.Out.Select( + hands => hands.Left, + DeliveryPolicy.SynchronousOrThrottle, + "SelectLeftHand").Out; /// /// Gets the stream of right hand information. /// - public Emitter Right { get; } + public Emitter Right => this.rightHandEmitter ??= this.Out.Select( + hands => hands.Right, + DeliveryPolicy.SynchronousOrThrottle, + "SelectRightHand").Out; /// public void Start(Action notifyCompletionTime) @@ -68,12 +74,7 @@ public override void Step() if (this.active && currentSampleTime - this.Out.LastEnvelope.OriginatingTime >= this.interval) { - var leftHand = PsiInput.LeftHand; - var rightHand = PsiInput.RightHand; - - this.Left.Post(leftHand, currentSampleTime); - this.Right.Post(rightHand, currentSampleTime); - this.Out.Post((leftHand, rightHand), currentSampleTime); + this.Out.Post((PsiInput.LeftHand, PsiInput.RightHand), currentSampleTime); } } } diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality/HeadSensor.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/HeadSensor.cs similarity index 97% rename from Sources/MixedReality/Microsoft.Psi.MixedReality/HeadSensor.cs rename to Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/HeadSensor.cs index e83e39871..2c380e829 100644 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality/HeadSensor.cs +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/HeadSensor.cs @@ -1,12 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -namespace Microsoft.Psi.MixedReality +namespace Microsoft.Psi.MixedReality.StereoKit { using System; using MathNet.Spatial.Euclidean; using Microsoft.Psi.Components; - using StereoKit; /// /// Source component that produces a stream of head coordinates. diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality/Microphone.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/Microphone.cs similarity index 81% rename from Sources/MixedReality/Microsoft.Psi.MixedReality/Microphone.cs rename to Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/Microphone.cs index 33645d5ef..2cefdc108 100644 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality/Microphone.cs +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/Microphone.cs @@ -1,13 +1,12 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -namespace Microsoft.Psi.MixedReality +namespace Microsoft.Psi.MixedReality.StereoKit { using System; using Microsoft.Psi; using Microsoft.Psi.Audio; using Microsoft.Psi.Components; - using SKMicrophone = StereoKit.Microphone; /// /// Component that captures audio from the microphone. @@ -43,7 +42,7 @@ public Microphone(Pipeline pipeline, MicrophoneConfiguration configuration = nul /// public override bool Initialize() { - if (!SKMicrophone.Start()) + if (!global::StereoKit.Microphone.Start()) { throw new Exception("Failed to access the system's default microphone."); } @@ -52,7 +51,7 @@ public override bool Initialize() } /// - public override void Shutdown() => SKMicrophone.Stop(); + public override void Shutdown() => global::StereoKit.Microphone.Stop(); /// public void Start(Action notifyCompletionTime) @@ -71,13 +70,13 @@ public void Stop(DateTime finalOriginatingTime, Action notifyCompleted) /// public unsafe override void Step() { - int unreadSamples = SKMicrophone.Sound.UnreadSamples; + int unreadSamples = global::StereoKit.Microphone.Sound.UnreadSamples; var originatingTime = this.pipeline.GetCurrentTime(); // Check if there are samples to capture. // Note that we wait for more than 1 unread sample to be available, otherwise the // ReadSamples() method below does not work as expected. - if (this.active && SKMicrophone.IsRecording && unreadSamples > 1) + if (this.active && global::StereoKit.Microphone.IsRecording && unreadSamples > 1) { // Ensure that the sample buffer is large enough if (unreadSamples > this.buffer.Length) @@ -86,14 +85,7 @@ public unsafe override void Step() } // Read the audio samples - int samples = SKMicrophone.Sound.ReadSamples(ref this.buffer); - - if (samples < unreadSamples && samples < this.buffer.Length) - { - throw new Exception( - "Error reading audio samples from the microphone.\n" + - $"Expected at least {Math.Min(unreadSamples, this.buffer.Length)} samples but obtained {samples}."); - } + int samples = global::StereoKit.Microphone.Sound.ReadSamples(ref this.buffer); // Convert to bytes and post the AudioBuffer byte[] audio = new byte[samples * this.audioFormat.BitsPerSample / 8]; diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality/MicrophoneConfiguration.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/MicrophoneConfiguration.cs similarity index 94% rename from Sources/MixedReality/Microsoft.Psi.MixedReality/MicrophoneConfiguration.cs rename to Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/MicrophoneConfiguration.cs index a6df41d64..a46f030e8 100644 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality/MicrophoneConfiguration.cs +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/MicrophoneConfiguration.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -namespace Microsoft.Psi.MixedReality +namespace Microsoft.Psi.MixedReality.StereoKit { using System; using Microsoft.Psi.Audio; diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/Operators.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/Operators.cs new file mode 100644 index 000000000..62df670b8 --- /dev/null +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/Operators.cs @@ -0,0 +1,157 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.Psi.MixedReality.StereoKit +{ + using System; + using System.Numerics; + using global::StereoKit; + using MathNet.Spatial.Euclidean; + using static Microsoft.Psi.Spatial.Euclidean.Operators; + using SystemDrawingColor = System.Drawing.Color; + + /// + /// Implements operators. + /// + public static partial class Operators + { + /// + /// Converts a StereoKit to a , + /// changing basis from HoloLens to MathNet. + /// + /// The to be converted. + /// The . + /// + /// The HoloLens basis assumes that Forward=-Z, Left=-X, and Up=Y. + /// The MathNet basis assumes that Forward=X, Left=Y, and Up=Z. + /// + public static CoordinateSystem ToCoordinateSystem(this Matrix stereoKitMatrix) + { + Matrix4x4 systemMatrix = stereoKitMatrix; + return systemMatrix.RebaseToMathNetCoordinateSystem(); + } + + /// + /// Converts a StereoKit to a , + /// changing basis from HoloLens to MathNet. + /// + /// The StereoKit to be converted. + /// The . + /// + /// The HoloLens basis assumes that Forward=-Z, Left=-X, and Up=Y. + /// The MathNet basis assumes that Forward=X, Left=Y, and Up=Z. + /// + public static CoordinateSystem ToCoordinateSystem(this Pose pose) + => pose.ToMatrix().ToCoordinateSystem(); + + /// + /// Converts a to a StereoKit , + /// changing basis from MathNet to HoloLens. + /// + /// The to be converted. + /// The . + /// + /// The HoloLens basis assumes that Forward=-Z, Left=-X, and Up=Y. + /// The MathNet basis assumes that Forward=X, Left=Y, and Up=Z. + /// + public static Matrix ToStereoKitMatrix(this CoordinateSystem coordinateSystem) + => new (coordinateSystem.RebaseToHoloLensSystemMatrix()); + + /// + /// Converts a pose to a StereoKit , + /// changing basis from MathNet to HoloLens. + /// + /// The pose to be converted. + /// The StereoKit . + /// + /// The HoloLens basis assumes that Forward=-Z, Left=-X, and Up=Y. + /// The MathNet basis assumes that Forward=X, Left=Y, and Up=Z. + /// + public static Pose ToStereoKitPose(this CoordinateSystem coordinateSystem) + => coordinateSystem.ToStereoKitMatrix().Pose; + + /// + /// Convert to StereoKit , changing the basis from MathNet to HoloLens. + /// + /// to be converted. + /// The StereoKit . + /// + /// The HoloLens basis assumes that Forward=-Z, Left=-X, and Up=Y. + /// The MathNet basis assumes that Forward=X, Left=Y, and Up=Z. + /// + public static Vec3 ToVec3(this Point3D point3d) + => new (-(float)point3d.Y, (float)point3d.Z, -(float)point3d.X); + + /// + /// Convert StereoKit to , changing the basis from HoloLens to MathNet. + /// + /// The StereoKit to be converted. + /// . + /// + /// The HoloLens basis assumes that Forward=-Z, Left=-X, and Up=Y. + /// The MathNet basis assumes that Forward=X, Left=Y, and Up=Z. + /// + public static Point3D ToPoint3D(this Vec3 vec3) + => new (-vec3.z, -vec3.x, vec3.y); + + /// + /// Converts a specified to a StereoKit . + /// + /// The . + /// The corresponding StereoKit . + public static Color ToStereoKitColor(this SystemDrawingColor color) + => new ((float)color.R / 255, (float)color.G / 255, (float)color.B / 255, (float)color.A / 255); + + /// + /// Converts a specified to a StereoKit . + /// + /// The . + /// The corresponding . + public static Color32 ToStereoKitColor32(this SystemDrawingColor color) + => new (color.R, color.G, color.B, color.A); + + /// + /// Gets the current StereoKit frame time from OpenXR as a \psi pipeline . + /// + /// The pipeline to get the current time for. + /// The current frame time. + public static DateTime GetCurrentTimeFromOpenXr(this Pipeline pipeline) + => pipeline.ConvertTimeFromOpenXr(Backend.OpenXR.Time); + + /// + /// Interpolates between two poses by interpolating the + /// poses of each joint. Spherical linear interpolation () + /// is used for rotation, and linear interpolation is used for translation. + /// + /// The first pose. + /// The second pose. + /// The amount to interpolate between the two hand poses. + /// A value between 0 and 1 will return an interpolation between the two values. + /// A value outside the 0-1 range will generate an extrapolated result. + /// The interpolated pose. + /// Returns null if either input hand is null. + public static Hand InterpolateHands(Hand hand1, Hand hand2, double amount) + { + if (hand1 is null || hand2 is null) + { + return null; + } + + // Interpolate each joint pose separately + int numJoints = (int)HandJointIndex.MaxIndex; + var interpolatedJoints = new CoordinateSystem[numJoints]; + for (int i = 0; i < numJoints; i++) + { + interpolatedJoints[i] = InterpolateCoordinateSystems(hand1.Joints[i], hand2.Joints[i], amount); + } + + // Use values from hand2 for boolean members if the amount we are + // interpolating is greater than 0.5. Otherwise select from hand1. + return new Hand( + amount > 0.5 ? hand2.IsTracked : hand1.IsTracked, + amount > 0.5 ? hand2.IsPinched : hand1.IsPinched, + amount > 0.5 ? hand2.IsGripped : hand1.IsGripped, + interpolatedJoints); + } + } +} diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality/PsiInput.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/PsiInput.cs similarity index 93% rename from Sources/MixedReality/Microsoft.Psi.MixedReality/PsiInput.cs rename to Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/PsiInput.cs index 1bc17dfd3..05e5ab855 100644 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality/PsiInput.cs +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/PsiInput.cs @@ -1,10 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -namespace Microsoft.Psi.MixedReality +namespace Microsoft.Psi.MixedReality.StereoKit { + using global::StereoKit; using MathNet.Spatial.Euclidean; - using StereoKit; /// /// Implements static properties for accessing inputs (head, eyes, and hands) with \psi conventions and types. @@ -26,7 +26,7 @@ public static Ray3D Eyes get { var cs = Input.Eyes.ToPsi(); - return new Ray3D(cs.Origin, cs.XAxis); + return cs is null ? default : new Ray3D(cs.Origin, cs.XAxis); } } @@ -42,7 +42,7 @@ public static Ray3D Eyes /// This input is also emitted on a stream by the component. public static Hand RightHand => Input.Hand(Handed.Right).ToPsi(); - private static Hand ToPsi(this StereoKit.Hand stereoKitHand) + private static Hand ToPsi(this global::StereoKit.Hand stereoKitHand) { var joints = new CoordinateSystem[(int)HandJointIndex.MaxIndex]; @@ -81,6 +81,7 @@ private static Hand ToPsi(this StereoKit.Hand stereoKitHand) return new Hand(stereoKitHand.IsTracked, stereoKitHand.IsPinched, stereoKitHand.IsGripped, joints); } - private static CoordinateSystem ToPsi(this Pose pose) => pose.ToCoordinateSystem().TransformBy(StereoKitTransforms.StereoKitToWorld); + private static CoordinateSystem ToPsi(this Pose pose) => + StereoKitTransforms.StereoKitToWorld is null ? null : pose.ToCoordinateSystem().TransformBy(StereoKitTransforms.StereoKitToWorld); } } diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality/Renderers/Box3DStereoKitRenderer.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/Renderers/Box3DRenderer.cs similarity index 73% rename from Sources/MixedReality/Microsoft.Psi.MixedReality/Renderers/Box3DStereoKitRenderer.cs rename to Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/Renderers/Box3DRenderer.cs index acc211b2e..36759247b 100644 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality/Renderers/Box3DStereoKitRenderer.cs +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/Renderers/Box3DRenderer.cs @@ -1,37 +1,37 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -namespace Microsoft.Psi.MixedReality +namespace Microsoft.Psi.MixedReality.StereoKit { + using global::StereoKit; using MathNet.Spatial.Euclidean; using Microsoft.Psi.Spatial.Euclidean; - using StereoKit; using Color = System.Drawing.Color; /// - /// Component that visually renders a 3D box. + /// Component that visually renders a . /// - public class Box3DStereoKitRenderer : MeshStereoKitRenderer, IConsumer + public class Box3DRenderer : MeshRenderer, IConsumer { private readonly bool roundedEdges = false; private readonly float roundedEdgeRadius; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The pipeline to add the component to. /// Box color. /// Whether to render as wireframe only. /// Visibility. /// An optional name for the component. - public Box3DStereoKitRenderer(Pipeline pipeline, Color color, bool wireframe = false, bool visible = true, string name = nameof(Box3DStereoKitRenderer)) + public Box3DRenderer(Pipeline pipeline, Color color, bool wireframe = false, bool visible = true, string name = nameof(Box3DRenderer)) : base(pipeline, null, color, wireframe, visible, name) { this.In = pipeline.CreateReceiver(this, this.UpdateMesh, nameof(this.In)); } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The pipeline to add the component to. /// Box to render. @@ -39,14 +39,14 @@ public Box3DStereoKitRenderer(Pipeline pipeline, Color color, bool wireframe = f /// Whether to render as wireframe only. /// Visibility. /// An optional name for the component. - public Box3DStereoKitRenderer(Pipeline pipeline, Box3D box3D, Color color, bool wireframe = false, bool visible = true, string name = nameof(Box3DStereoKitRenderer)) + public Box3DRenderer(Pipeline pipeline, Box3D box3D, Color color, bool wireframe = false, bool visible = true, string name = nameof(Box3DRenderer)) : this(pipeline, color, wireframe, visible, name) { this.UpdateMesh(box3D); } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The pipeline to add the component to. /// Box color. @@ -54,7 +54,7 @@ public Box3DStereoKitRenderer(Pipeline pipeline, Box3D box3D, Color color, bool /// Whether to render as wireframe only. /// Visibility. /// An optional name for the component. - public Box3DStereoKitRenderer(Pipeline pipeline, Color color, float roundedEdgeRadius, bool wireframe = false, bool visible = true, string name = nameof(Box3DStereoKitRenderer)) + public Box3DRenderer(Pipeline pipeline, Color color, float roundedEdgeRadius, bool wireframe = false, bool visible = true, string name = nameof(Box3DRenderer)) : this(pipeline, color, wireframe, visible, name) { this.roundedEdges = true; @@ -62,7 +62,7 @@ public Box3DStereoKitRenderer(Pipeline pipeline, Color color, float roundedEdgeR } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The pipeline to add the component to. /// Box to render. @@ -71,7 +71,7 @@ public Box3DStereoKitRenderer(Pipeline pipeline, Color color, float roundedEdgeR /// Whether to render as wireframe only. /// Visibility. /// An optional name for the component. - public Box3DStereoKitRenderer(Pipeline pipeline, Box3D box3D, Color color, float roundedEdgeRadius, bool wireframe = false, bool visible = true, string name = nameof(Box3DStereoKitRenderer)) + public Box3DRenderer(Pipeline pipeline, Box3D box3D, Color color, float roundedEdgeRadius, bool wireframe = false, bool visible = true, string name = nameof(Box3DRenderer)) : this(pipeline, color, wireframe, visible, name) { this.roundedEdges = true; diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality/Renderers/EncodedImageRectangle3DStereoKitRenderer.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/Renderers/EncodedImageRectangle3DRenderer.cs similarity index 77% rename from Sources/MixedReality/Microsoft.Psi.MixedReality/Renderers/EncodedImageRectangle3DStereoKitRenderer.cs rename to Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/Renderers/EncodedImageRectangle3DRenderer.cs index bdf47743e..a8bdb8303 100644 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality/Renderers/EncodedImageRectangle3DStereoKitRenderer.cs +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/Renderers/EncodedImageRectangle3DRenderer.cs @@ -1,37 +1,37 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -namespace Microsoft.Psi.MixedReality +namespace Microsoft.Psi.MixedReality.StereoKit { + using global::StereoKit; using Microsoft.Psi.Imaging; using Microsoft.Psi.Spatial.Euclidean; - using StereoKit; /// /// Component that visually renders an encoded image on a 3D rectangle. /// - public class EncodedImageRectangle3DStereoKitRenderer : Rectangle3DStereoKitRenderer + public class EncodedImageRectangle3DRenderer : Rectangle3DRenderer { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The pipeline to add the component to. /// Visibility. /// An optional name for the component. - public EncodedImageRectangle3DStereoKitRenderer(Pipeline pipeline, bool visible = true, string name = nameof(EncodedImageRectangle3DStereoKitRenderer)) + public EncodedImageRectangle3DRenderer(Pipeline pipeline, bool visible = true, string name = nameof(EncodedImageRectangle3DRenderer)) : base(pipeline, System.Drawing.Color.White, false, visible, name) { this.Image = pipeline.CreateReceiver>(this, this.UpdateImage, nameof(this.Image)); } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The pipeline to add the component to. /// Rectangle to render. /// Visibility. /// An optional name for the component. - public EncodedImageRectangle3DStereoKitRenderer(Pipeline pipeline, Rectangle3D rectangle3D, bool visible = true, string name = nameof(EncodedImageRectangle3DStereoKitRenderer)) + public EncodedImageRectangle3DRenderer(Pipeline pipeline, Rectangle3D rectangle3D, bool visible = true, string name = nameof(EncodedImageRectangle3DRenderer)) : base(pipeline, rectangle3D, System.Drawing.Color.White, false, visible, name) { this.Image = pipeline.CreateReceiver>(this, this.UpdateImage, nameof(this.Image)); diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality/Renderers/HandsStereoKitRenderer.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/Renderers/HandsRenderer.cs similarity index 87% rename from Sources/MixedReality/Microsoft.Psi.MixedReality/Renderers/HandsStereoKitRenderer.cs rename to Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/Renderers/HandsRenderer.cs index 947b5552a..4bdcc1a06 100644 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality/Renderers/HandsStereoKitRenderer.cs +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/Renderers/HandsRenderer.cs @@ -1,27 +1,27 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -namespace Microsoft.Psi.MixedReality +namespace Microsoft.Psi.MixedReality.StereoKit { - using StereoKit; + using global::StereoKit; using Color = System.Drawing.Color; /// /// Component that controls rendering of the hands in StereoKit. /// - public class HandsStereoKitRenderer + public class HandsRenderer { private readonly string name; private readonly Material material = Default.MaterialHand; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The pipeline to add the component to. /// Initial value indicating whether hands should be rendered (default true). /// Initial value indicating whether hands participate in StereoKit physics (default false). /// An optional name for the component. - public HandsStereoKitRenderer(Pipeline pipeline, bool visible = true, bool solid = false, string name = nameof(HandsStereoKitRenderer)) + public HandsRenderer(Pipeline pipeline, bool visible = true, bool solid = false, string name = nameof(HandsRenderer)) { this.name = name; this.ReceiveVisible(visible); diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality/Renderers/Mesh3DStereoKitRenderer.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/Renderers/Mesh3DRenderer.cs similarity index 73% rename from Sources/MixedReality/Microsoft.Psi.MixedReality/Renderers/Mesh3DStereoKitRenderer.cs rename to Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/Renderers/Mesh3DRenderer.cs index e4cd42213..63f740bc7 100644 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality/Renderers/Mesh3DStereoKitRenderer.cs +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/Renderers/Mesh3DRenderer.cs @@ -1,34 +1,34 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -namespace Microsoft.Psi.MixedReality +namespace Microsoft.Psi.MixedReality.StereoKit { using System.Linq; + using global::StereoKit; using Microsoft.Psi.Spatial.Euclidean; - using StereoKit; using Color = System.Drawing.Color; /// /// Component that visually renders a . /// - public class Mesh3DStereoKitRenderer : MeshStereoKitRenderer, IConsumer + public class Mesh3DRenderer : MeshRenderer, IConsumer { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The pipeline to add the component to. /// Mesh color. /// Whether to render as wireframe only. /// Visibility. /// An optional name for the component. - public Mesh3DStereoKitRenderer(Pipeline pipeline, Color color, bool wireframe = false, bool visible = true, string name = nameof(Mesh3DStereoKitRenderer)) + public Mesh3DRenderer(Pipeline pipeline, Color color, bool wireframe = false, bool visible = true, string name = nameof(Mesh3DRenderer)) : base(pipeline, null, color, wireframe, visible, name) { this.In = pipeline.CreateReceiver(this, this.UpdateMesh, nameof(this.In)); } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The pipeline to add the component to. /// Mesh to render. @@ -36,7 +36,7 @@ public Mesh3DStereoKitRenderer(Pipeline pipeline, Color color, bool wireframe = /// Whether to render as wireframe only. /// Visibility. /// An optional name for the component. - public Mesh3DStereoKitRenderer(Pipeline pipeline, Mesh3D mesh3D, Color color, bool wireframe = false, bool visible = true, string name = nameof(Mesh3DStereoKitRenderer)) + public Mesh3DRenderer(Pipeline pipeline, Mesh3D mesh3D, Color color, bool wireframe = false, bool visible = true, string name = nameof(Mesh3DRenderer)) : this(pipeline, color, wireframe, visible, name) { this.UpdateMesh(mesh3D); diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality/Renderers/MeshStereoKitRenderer.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/Renderers/MeshRenderer.cs similarity index 89% rename from Sources/MixedReality/Microsoft.Psi.MixedReality/Renderers/MeshStereoKitRenderer.cs rename to Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/Renderers/MeshRenderer.cs index 35ad84155..4bb74261d 100644 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality/Renderers/MeshStereoKitRenderer.cs +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/Renderers/MeshRenderer.cs @@ -1,24 +1,24 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -namespace Microsoft.Psi.MixedReality +namespace Microsoft.Psi.MixedReality.StereoKit { using System.IO; using System.Reflection; + using global::StereoKit; using MathNet.Spatial.Euclidean; - using StereoKit; using Color = System.Drawing.Color; /// - /// Component that visually renders a single mesh. + /// Component that visually renders a single . /// - public class MeshStereoKitRenderer : StereoKitRenderer + public class MeshRenderer : StereoKitRenderer { private Matrix pose = Matrix.Identity; private Matrix scale = Matrix.Identity; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The pipeline to add the component to. /// The mesh to render. @@ -28,7 +28,7 @@ public class MeshStereoKitRenderer : StereoKitRenderer /// Whether to render mesh as wireframe only. /// Visibility. /// An optional name for the component. - public MeshStereoKitRenderer(Pipeline pipeline, Mesh mesh, CoordinateSystem pose, Vector3D scale, Color color, bool wireframe = false, bool visible = true, string name = nameof(MeshStereoKitRenderer)) + public MeshRenderer(Pipeline pipeline, Mesh mesh, CoordinateSystem pose, Vector3D scale, Color color, bool wireframe = false, bool visible = true, string name = nameof(MeshRenderer)) : base(pipeline, name) { this.Mesh = mesh; @@ -50,7 +50,7 @@ public MeshStereoKitRenderer(Pipeline pipeline, Mesh mesh, CoordinateSystem pose } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The pipeline to add the component to. /// The mesh to render. @@ -58,7 +58,7 @@ public MeshStereoKitRenderer(Pipeline pipeline, Mesh mesh, CoordinateSystem pose /// Whether to render mesh as wireframe only. /// Visibility. /// An optional name for the component. - public MeshStereoKitRenderer(Pipeline pipeline, Mesh mesh, Color color, bool wireframe = false, bool visible = true, string name = nameof(MeshStereoKitRenderer)) + public MeshRenderer(Pipeline pipeline, Mesh mesh, Color color, bool wireframe = false, bool visible = true, string name = nameof(MeshRenderer)) : this(pipeline, mesh, null, new Vector3D(1, 1, 1), color, wireframe, visible, name) { } diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality/Renderers/Rectangle3DStereoKitRenderer.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/Renderers/Rectangle3DRenderer.cs similarity index 76% rename from Sources/MixedReality/Microsoft.Psi.MixedReality/Renderers/Rectangle3DStereoKitRenderer.cs rename to Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/Renderers/Rectangle3DRenderer.cs index 2574481a5..6cac70143 100644 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality/Renderers/Rectangle3DStereoKitRenderer.cs +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/Renderers/Rectangle3DRenderer.cs @@ -1,33 +1,33 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -namespace Microsoft.Psi.MixedReality +namespace Microsoft.Psi.MixedReality.StereoKit { + using global::StereoKit; using Microsoft.Psi.Spatial.Euclidean; - using StereoKit; using Color = System.Drawing.Color; /// - /// Component that visually renders a 3D rectangle. + /// Component that visually renders a . /// - public class Rectangle3DStereoKitRenderer : MeshStereoKitRenderer, IConsumer + public class Rectangle3DRenderer : MeshRenderer, IConsumer { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The pipeline to add the component to. /// Rectangle color. /// Whether to render as wireframe only. /// Visibility. /// An optional name for the component. - public Rectangle3DStereoKitRenderer(Pipeline pipeline, Color color, bool wireframe = false, bool visible = true, string name = nameof(Rectangle3DStereoKitRenderer)) + public Rectangle3DRenderer(Pipeline pipeline, Color color, bool wireframe = false, bool visible = true, string name = nameof(Rectangle3DRenderer)) : base(pipeline, null, color, wireframe, visible, name) { this.In = pipeline.CreateReceiver(this, this.UpdateMesh, nameof(this.In)); } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The pipeline to add the component to. /// Rectangle to render. @@ -35,7 +35,7 @@ public Rectangle3DStereoKitRenderer(Pipeline pipeline, Color color, bool wirefra /// Whether to render as wireframe only. /// Visibility. /// An optional name for the component. - public Rectangle3DStereoKitRenderer(Pipeline pipeline, Rectangle3D rectangle3D, Color color, bool wireframe = false, bool visible = true, string name = nameof(Rectangle3DStereoKitRenderer)) + public Rectangle3DRenderer(Pipeline pipeline, Rectangle3D rectangle3D, Color color, bool wireframe = false, bool visible = true, string name = nameof(Rectangle3DRenderer)) : this(pipeline, color, wireframe, visible, name) { this.UpdateMesh(rectangle3D); diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality/Renderers/StereoKitRenderer.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/Renderers/StereoKitRenderer.cs similarity index 77% rename from Sources/MixedReality/Microsoft.Psi.MixedReality/Renderers/StereoKitRenderer.cs rename to Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/Renderers/StereoKitRenderer.cs index 970e7800e..1013e14c3 100644 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality/Renderers/StereoKitRenderer.cs +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/Renderers/StereoKitRenderer.cs @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -namespace Microsoft.Psi.MixedReality +namespace Microsoft.Psi.MixedReality.StereoKit { - using StereoKit; + using global::StereoKit; /// /// Base class for StereoKit rendering components. @@ -24,9 +24,12 @@ public StereoKitRenderer(Pipeline pipeline, string name = nameof(StereoKitRender /// public override void Step() { - Hierarchy.Push(StereoKitTransforms.WorldHierarchy); - this.Render(); - Hierarchy.Pop(); + if (StereoKitTransforms.WorldHierarchy.HasValue) + { + Hierarchy.Push(StereoKitTransforms.WorldHierarchy.Value); + this.Render(); + Hierarchy.Pop(); + } } /// diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality/Renderers/TextStereoKitRenderer.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/Renderers/TextRenderer.cs similarity index 91% rename from Sources/MixedReality/Microsoft.Psi.MixedReality/Renderers/TextStereoKitRenderer.cs rename to Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/Renderers/TextRenderer.cs index 06151297b..cdad98a99 100644 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality/Renderers/TextStereoKitRenderer.cs +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/Renderers/TextRenderer.cs @@ -1,37 +1,37 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -namespace Microsoft.Psi.MixedReality +namespace Microsoft.Psi.MixedReality.StereoKit { + using global::StereoKit; using MathNet.Spatial.Euclidean; - using StereoKit; using Color = System.Drawing.Color; /// /// Component that visually renders text. /// - public class TextStereoKitRenderer : StereoKitRenderer + public class TextRenderer : StereoKitRenderer { private readonly Model billboardFillModel; private readonly Material billboardFillMaterial; private readonly (Vec3 startPoint, Vec3 endPoint)[] billboardBorderLines; - private readonly TextStereoKitRendererConfiguration configuration; + private readonly TextRendererConfiguration configuration; private Matrix pose; private TextStyle textStyle; - private StereoKit.Color billboardBorderColor; + private global::StereoKit.Color billboardBorderColor; private Matrix billboardFillTransform; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The pipeline to add the component to. /// Configuration to use. /// An optional name for the component. - public TextStereoKitRenderer(Pipeline pipeline, TextStereoKitRendererConfiguration configuration = null, string name = nameof(TextStereoKitRenderer)) + public TextRenderer(Pipeline pipeline, TextRendererConfiguration configuration = null, string name = nameof(TextRenderer)) : base(pipeline, name) { - this.configuration = configuration ?? new TextStereoKitRendererConfiguration(); + this.configuration = configuration ?? new TextRendererConfiguration(); this.pose = this.configuration.Pose.ToStereoKitMatrix(); this.Pose = pipeline.CreateReceiver(this, this.ReceivePose, nameof(this.Pose)); @@ -120,10 +120,10 @@ protected override void Render() if (!string.IsNullOrEmpty(this.configuration.Text)) { // Render the text - StereoKit.Text.Add( + global::StereoKit.Text.Add( this.configuration.Text, Matrix.Identity, - this.configuration.DesiredTextSize ?? StereoKit.Text.Size(this.configuration.Text), + this.configuration.DesiredTextSize ?? global::StereoKit.Text.Size(this.configuration.Text), this.configuration.TextFit, this.textStyle, this.configuration.TextPosition, @@ -157,7 +157,7 @@ private void ReceiveTextColor(Color color) private void UpdateTextStyle() { var colorGamma = this.configuration.TextColor.ToStereoKitColor().ToGamma(); - this.textStyle = StereoKit.Text.MakeStyle(this.configuration.TextFont, TextStyle.Default.CharHeight, colorGamma); + this.textStyle = global::StereoKit.Text.MakeStyle(this.configuration.TextFont, TextStyle.Default.CharHeight, colorGamma); } private void ReceiveBillboardSize(Vec2 size) diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality/Renderers/TextStereoKitRendererConfiguration.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/Renderers/TextRendererConfiguration.cs similarity index 94% rename from Sources/MixedReality/Microsoft.Psi.MixedReality/Renderers/TextStereoKitRendererConfiguration.cs rename to Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/Renderers/TextRendererConfiguration.cs index d8fbb6f5a..d0101b6c1 100644 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality/Renderers/TextStereoKitRendererConfiguration.cs +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/Renderers/TextRendererConfiguration.cs @@ -1,18 +1,18 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -namespace Microsoft.Psi.MixedReality +namespace Microsoft.Psi.MixedReality.StereoKit { + using global::StereoKit; using MathNet.Spatial.Euclidean; - using StereoKit; using Color = System.Drawing.Color; /// - /// Represents the configuration for a component. + /// Represents the configuration for a component. /// /// This configuration object is used when initializing a new renderer instance, but many of the parameters can /// be subsequently changed based on streaming inputs to the component. - public class TextStereoKitRendererConfiguration + public class TextRendererConfiguration { /// /// Gets or sets the text to render. diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality/SpatialSound.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/SpatialSound.cs similarity index 78% rename from Sources/MixedReality/Microsoft.Psi.MixedReality/SpatialSound.cs rename to Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/SpatialSound.cs index ac45a4f51..f88a59571 100644 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality/SpatialSound.cs +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/SpatialSound.cs @@ -1,13 +1,13 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -namespace Microsoft.Psi.MixedReality +namespace Microsoft.Psi.MixedReality.StereoKit { using System; using System.IO; + using global::StereoKit; using MathNet.Spatial.Euclidean; using Microsoft.Psi.Audio; - using StereoKit; /// /// Component that implements a spatial sound renderer. @@ -16,7 +16,7 @@ public class SpatialSound : StereoKitComponent, IConsumer { private Sound sound; private SoundInst soundInst; - private Vec3 position; + private Point3D worldPosition; private float volume; private bool playing = false; @@ -30,11 +30,11 @@ public class SpatialSound : StereoKitComponent, IConsumer public SpatialSound(Pipeline pipeline, Point3D initialPosition, double initialVolume = 1, string name = nameof(SpatialSound)) : base(pipeline, name) { - this.position = new CoordinateSystem(initialPosition, UnitVector3D.XAxis, UnitVector3D.YAxis, UnitVector3D.ZAxis).ToStereoKitMatrix().Translation; + this.worldPosition = initialPosition; this.volume = (float)initialVolume; this.In = pipeline.CreateReceiver(this, this.UpdateAudio, nameof(this.In)); - this.PositionInput = pipeline.CreateReceiver(this, this.UpdatePosition, nameof(this.PositionInput)); - this.VolumeInput = pipeline.CreateReceiver(this, this.UpdateVolume, nameof(this.VolumeInput)); + this.PositionInput = pipeline.CreateReceiver(this, p => this.worldPosition = p, nameof(this.PositionInput)); + this.VolumeInput = pipeline.CreateReceiver(this, v => this.volume = (float)v, nameof(this.VolumeInput)); } /// @@ -59,6 +59,32 @@ public override bool Initialize() return true; } + /// + public override void Step() + { + if (this.playing) + { + this.soundInst.Volume = this.volume; + + if (StereoKitTransforms.WorldToStereoKit is not null) + { + this.soundInst.Position = this.ComputeSoundPosition(); + } + } + } + + private Vec3 ComputeSoundPosition() + { + if (StereoKitTransforms.WorldToStereoKit is null) + { + return Vec3.Zero; + } + else + { + return this.worldPosition.TransformBy(StereoKitTransforms.WorldToStereoKit).ToVec3(); + } + } + private void UpdateAudio(AudioBuffer audio) { var format = audio.Format; @@ -83,27 +109,9 @@ private void UpdateAudio(AudioBuffer audio) this.sound.WriteSamples(samples); if (!this.playing) { - this.soundInst = this.sound.Play(this.position, this.volume); + this.soundInst = this.sound.Play(this.ComputeSoundPosition(), this.volume); this.playing = true; } } - - private void UpdatePosition(Point3D position) - { - this.position = position.TransformBy(StereoKitTransforms.WorldToStereoKit).ToVec3(); - if (this.playing) - { - this.soundInst.Position = this.position; - } - } - - private void UpdateVolume(double volume) - { - this.volume = (float)volume; - if (this.playing) - { - this.soundInst.Volume = this.volume; - } - } } } diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKitComponent.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/StereoKitComponent.cs similarity index 94% rename from Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKitComponent.cs rename to Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/StereoKitComponent.cs index d81817d0c..079014d30 100644 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKitComponent.cs +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/StereoKitComponent.cs @@ -1,11 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -namespace Microsoft.Psi.MixedReality +namespace Microsoft.Psi.MixedReality.StereoKit { using System; - using StereoKit; - using StereoKit.Framework; + using global::StereoKit; + using global::StereoKit.Framework; /// /// Base abstract class for implementing StereoKit \psi components. diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKitTransforms.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/StereoKitTransforms.cs similarity index 83% rename from Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKitTransforms.cs rename to Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/StereoKitTransforms.cs index fa1e2d6a5..d7e2201d7 100644 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKitTransforms.cs +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality/StereoKit/StereoKitTransforms.cs @@ -1,10 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -namespace Microsoft.Psi.MixedReality +namespace Microsoft.Psi.MixedReality.StereoKit { + using global::StereoKit; using MathNet.Spatial.Euclidean; - using StereoKit; /// /// Static StereoKit transforms which are applied in/out of StereoKit from \psi. @@ -17,8 +17,9 @@ public static class StereoKitTransforms /// /// /// This matrix is pushed automatically by the base class for new rendering components. + /// The value is null when the HoloLens loses localization. /// - public static Matrix WorldHierarchy { get; internal set; } = Matrix.Identity; + public static Matrix? WorldHierarchy { get; internal set; } = Matrix.Identity; /// /// Gets or sets the transform from StereoKit to the world. diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality/TimeHelper.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality/TimeHelper.cs index 572aa12bf..e352ab1ec 100644 --- a/Sources/MixedReality/Microsoft.Psi.MixedReality/TimeHelper.cs +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality/TimeHelper.cs @@ -5,7 +5,7 @@ namespace Microsoft.Psi.MixedReality { using System; using System.Runtime.InteropServices; - using StereoKit; + using global::StereoKit; /// /// Provides helper methods for converting between OpenXR and platform times. @@ -13,7 +13,9 @@ namespace Microsoft.Psi.MixedReality internal static class TimeHelper { private static readonly double QpcToHns; + private static readonly double HnsToQpc; private static XR_xrConvertTimeToWin32PerformanceCounterKHR openXrConvertTimeToWin32PerformanceCounterKHR; + private static XR_xrConvertWin32PerformanceCounterToTimeKHR openXrConvertWin32PerformanceCounterToTimeKHR; /// /// Initializes static members of the class. @@ -22,22 +24,45 @@ static TimeHelper() { NativeMethods.QueryPerformanceFrequency(out long frequency); QpcToHns = 10000000.0 / frequency; + HnsToQpc = frequency / 10000000.0; } private delegate int XR_xrConvertTimeToWin32PerformanceCounterKHR(ulong instance, long time, out long performanceCount); + private delegate int XR_xrConvertWin32PerformanceCounterToTimeKHR(ulong instance, ref long performanceCount, out long time); + /// - /// Converts an OpenXR time value as returned from Backend.OpenXR.Time to 100 ns ticks based on the performance counter. + /// Converts an OpenXR time value (e.g., from Backend.OpenXR.Time) to 100 ns ticks based on the performance counter. /// /// The OpenXR time value. - /// The equivalent performance counter value in 100 ns ticks. + /// The equivalent time value in 100 ns ticks. internal static long ConvertXrTimeToHnsTicks(long openXrTime) { return (long)(ConvertXrTimeToWin32PerformanceCounter(openXrTime) * QpcToHns); } /// - /// Converts an OpenXR time value as returned from Backend.OpenXR.Time to a performance counter value as if generated by the QueryPerformanceCounter function. + /// Converts a time value expressed in 100 ns ticks to the equivalent OpenXR time based on the performance counter. + /// + /// The time in 100 ns ticks. + /// The equivalent OpenXR time. + internal static long ConvertHnsTicksToXrTime(long hnsTicks) + { + return ConvertWin32PerformanceCounterToXrTime((long)(hnsTicks * HnsToQpc)); + } + + /// + /// Gets the current time from the QueryPerformanceCounter function and converts to 100 ns ticks. + /// + /// The current time expressed in 100 ns ticks. + internal static long GetCurrentTimeHnsTicks() + { + NativeMethods.QueryPerformanceCounter(out long qpc); + return (long)(qpc * QpcToHns); + } + + /// + /// Converts an OpenXR time value (e.g., from Backend.OpenXR.Time) to a performance counter value as if generated by the QueryPerformanceCounter function. /// /// The OpenXR time value. /// The equivalent performance counter value. @@ -65,6 +90,35 @@ private static long ConvertXrTimeToWin32PerformanceCounter(long openXrTime) return performanceCount; } + /// + /// Converts a performance counter value to an OpenXR time value. + /// + /// The performance counter value. + /// The equivalent OpenXR time value. + private static long ConvertWin32PerformanceCounterToXrTime(long performanceCount) + { + // Initialize the delegate on first use + if (openXrConvertWin32PerformanceCounterToTimeKHR == null) + { + if (!SK.IsInitialized) + { + throw new InvalidOperationException("Attempting to convert to OpenXR time before StereoKit is initialized. Ensure that SK.Initialize() has been called first."); + } + + if (Backend.XRType != BackendXRType.OpenXR) + { + throw new InvalidOperationException("Cannot convert to OpenXR time. Backend XR type is not OpenXR."); + } + + openXrConvertWin32PerformanceCounterToTimeKHR = Backend.OpenXR.GetFunction("xrConvertWin32PerformanceCounterToTimeKHR"); + } + + // Convert performance counter to xr time + openXrConvertWin32PerformanceCounterToTimeKHR(Backend.OpenXR.Instance, ref performanceCount, out long openXrTime); + + return openXrTime; + } + /// /// Provides native APIs used by the class. /// diff --git a/Sources/MixedReality/Microsoft.Psi.MixedReality/WinRT/Eyes.cs b/Sources/MixedReality/Microsoft.Psi.MixedReality/WinRT/Eyes.cs new file mode 100644 index 000000000..faee76278 --- /dev/null +++ b/Sources/MixedReality/Microsoft.Psi.MixedReality/WinRT/Eyes.cs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.Psi.MixedReality.WinRT +{ + using System; + using MathNet.Spatial.Euclidean; + using Microsoft.Psi.Common; + using Microsoft.Psi.Serialization; + + /// + /// Represents the tracked pose of the user's eye gaze, as produced by the WinRT-based EyesSensor component. + /// + [Serializer(typeof(Eyes.CustomSerializer))] + public class Eyes + { + /// + /// Initializes a new instance of the class. + /// + /// The 3D ray representing the eye-gaze pose. + /// Value indicating whether or not the calibration was valid for this eye-gaze pose. + public Eyes(Ray3D? gazeRay, bool calibrationValid) + { + this.GazeRay = gazeRay; + this.CalibrationValid = calibrationValid; + } + + /// + /// Gets the eye-gaze pose as a 3D ray. + /// + public Ray3D? GazeRay { get; private set; } + + /// + /// Gets a value indicating whether or not the calibration was valid for this eye-gaze pose. + /// + public bool CalibrationValid { get; private set; } + + /// + /// Provides custom read- backcompat serialization for objects. + /// + public class CustomSerializer : BackCompatClassSerializer + { + // When introducing a custom serializer, the LatestSchemaVersion + // is set to be one above the auto-generated schema version (given by + // RuntimeInfo.LatestSerializationSystemVersion, which was 2 at the time) + private const int LatestSchemaVersion = 3; + private SerializationHandler ray3DHandler; + private SerializationHandler boolHandler; + + /// + /// Initializes a new instance of the class. + /// + public CustomSerializer() + : base(LatestSchemaVersion) + { + } + + /// + public override void InitializeBackCompatSerializationHandlers(int schemaVersion, KnownSerializers serializers, TypeSchema targetSchema) + { + if (schemaVersion <= 2) + { + this.ray3DHandler = serializers.GetHandler(); + this.boolHandler = serializers.GetHandler(); + } + else + { + throw new NotSupportedException($"{nameof(Eyes.CustomSerializer)} only supports schema versions 2 and 3."); + } + } + + /// + public override void BackCompatDeserialize(int schemaVersion, BufferReader reader, ref Eyes target, SerializationContext context) + { + if (schemaVersion <= 2) + { + var gazeRay = default(Ray3D); + var calibrationValid = false; + this.ray3DHandler.Deserialize(reader, ref gazeRay, context); + this.boolHandler.Deserialize(reader, ref calibrationValid, context); + target = new Eyes(gazeRay, calibrationValid); + } + else + { + throw new NotSupportedException($"{nameof(Eyes.CustomSerializer)} only supports schema versions 2 and 3."); + } + } + } + } +} diff --git a/Sources/RealSense/Microsoft.Psi.RealSense.Windows.x64/Properties/AssemblyInfo.cs b/Sources/RealSense/Microsoft.Psi.RealSense.Windows.x64/Properties/AssemblyInfo.cs index b90a3070f..96f1f52ae 100644 --- a/Sources/RealSense/Microsoft.Psi.RealSense.Windows.x64/Properties/AssemblyInfo.cs +++ b/Sources/RealSense/Microsoft.Psi.RealSense.Windows.x64/Properties/AssemblyInfo.cs @@ -35,6 +35,6 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("0.17.52.1")] -[assembly: AssemblyFileVersion("0.17.52.1")] -[assembly: AssemblyInformationalVersion("0.17.52.1-beta")] +[assembly: AssemblyVersion("0.18.72.1")] +[assembly: AssemblyFileVersion("0.18.72.1")] +[assembly: AssemblyInformationalVersion("0.18.72.1-beta")] diff --git a/Sources/RealSense/Microsoft.Psi.RealSense_Interop.Windows.x64/AssemblyInfo.cpp b/Sources/RealSense/Microsoft.Psi.RealSense_Interop.Windows.x64/AssemblyInfo.cpp index ddb5bf116..a982a4318 100644 --- a/Sources/RealSense/Microsoft.Psi.RealSense_Interop.Windows.x64/AssemblyInfo.cpp +++ b/Sources/RealSense/Microsoft.Psi.RealSense_Interop.Windows.x64/AssemblyInfo.cpp @@ -31,9 +31,9 @@ using namespace System::Security::Permissions; // You can specify all the value or you can default the Revision and Build Numbers // by using the '*' as shown below: -[assembly:AssemblyVersionAttribute("0.17.52.1")]; -[assembly:AssemblyFileVersionAttribute("0.17.52.1")]; -[assembly:AssemblyInformationalVersionAttribute("0.17.52.1-beta")]; +[assembly:AssemblyVersionAttribute("0.18.72.1")]; +[assembly:AssemblyFileVersionAttribute("0.18.72.1")]; +[assembly:AssemblyInformationalVersionAttribute("0.18.72.1-beta")]; [assembly:ComVisible(false)]; diff --git a/Sources/Runtime/Microsoft.Psi.Interop/Rendezvous/Operators.cs b/Sources/Runtime/Microsoft.Psi.Interop/Rendezvous/Operators.cs index 9d7c3472d..7a1608399 100644 --- a/Sources/Runtime/Microsoft.Psi.Interop/Rendezvous/Operators.cs +++ b/Sources/Runtime/Microsoft.Psi.Interop/Rendezvous/Operators.cs @@ -22,10 +22,7 @@ public static class Operators /// The name of the rendezvous stream. /// Rendezvous endpoint. public static Rendezvous.Endpoint ToRendezvousEndpoint(this TcpWriter writer, string address, string streamName) - { - // Each TcpWriter is an endpoint emitting a single stream - return new Rendezvous.TcpSourceEndpoint(address, writer.Port, new[] { new Rendezvous.Stream(streamName, typeof(T)) }); - } + => new Rendezvous.TcpSourceEndpoint(address, writer.Port, new Rendezvous.Stream(streamName, typeof(T))); /// /// Create a from a . @@ -71,9 +68,7 @@ public static Rendezvous.Endpoint ToRendezvousEndpoint(this NetMQWriter writer) /// Host address with which to create endpoint. /// Rendezvous endpoint. public static Rendezvous.Endpoint ToRendezvousEndpoint(this RemoteClockExporter exporter, string host) - { - return new Rendezvous.RemoteClockExporterEndpoint(host, exporter.Port); - } + => new Rendezvous.RemoteClockExporterEndpoint(host, exporter.Port); /// /// Create a from a . @@ -86,7 +81,7 @@ public static Rendezvous.Endpoint ToRendezvousEndpoint(this RemoteClockExporter /// Flag indicating whether or not to post with originating times received over the socket. If false, we ignore them and instead use pipeline's current time. /// . public static NetMQSource ToNetMQSource(this Rendezvous.NetMQSourceEndpoint endpoint, Pipeline pipeline, string topic, IFormatDeserializer deserializer, bool useSourceOriginatingTimes = true) - => new NetMQSource(pipeline, topic, endpoint.Address, deserializer, useSourceOriginatingTimes); + => new (pipeline, topic, endpoint.Address, deserializer, useSourceOriginatingTimes); /// /// Create a rendezvous endpoint from a . @@ -113,9 +108,7 @@ public static Rendezvous.Endpoint ToRendezvousEndpoint(this RemoteExporter expor /// The pipeline to add the component to. /// . public static RemoteImporter ToRemoteImporter(this Rendezvous.RemoteExporterEndpoint endpoint, Pipeline pipeline) - { - return new RemoteImporter(pipeline, endpoint.Host, endpoint.Port); - } + => new (pipeline, endpoint.Host, endpoint.Port); /// /// Create a from a . @@ -124,8 +117,31 @@ public static RemoteImporter ToRemoteImporter(this Rendezvous.RemoteExporterEndp /// The pipeline to add the component to. /// . public static RemoteClockImporter ToRemoteClockImporter(this Rendezvous.RemoteClockExporterEndpoint endpoint, Pipeline pipeline) + => new (pipeline, endpoint.Host, endpoint.Port); + + /// + /// Writes a stream to a specified rendezvous process. + /// + /// The type of data in the stream. + /// The source stream to write. + /// The name under which to write the stream to the rendezvous process. + /// The rendezvous process. + /// The address to write the stream to. + /// The port to write the stream to. + /// The serializer to use when writing the stream. + /// An optional delivery policy. + public static void WriteToRendezvousProcess( + this IProducer source, + string streamName, + Rendezvous.Process rendezvousProcess, + string address, + int port, + IFormatSerializer serializer, + DeliveryPolicy deliveryPolicy = null) { - return new RemoteClockImporter(pipeline, endpoint.Host, endpoint.Port); + var tcpWriter = new TcpWriter(source.Out.Pipeline, port, serializer); + source.PipeTo(tcpWriter, deliveryPolicy); + rendezvousProcess.AddEndpoint(tcpWriter.ToRendezvousEndpoint(address, streamName)); } } } diff --git a/Sources/Runtime/Microsoft.Psi.Interop/Rendezvous/Rendezvous.cs b/Sources/Runtime/Microsoft.Psi.Interop/Rendezvous/Rendezvous.cs index 94b57769c..a910e257c 100644 --- a/Sources/Runtime/Microsoft.Psi.Interop/Rendezvous/Rendezvous.cs +++ b/Sources/Runtime/Microsoft.Psi.Interop/Rendezvous/Rendezvous.cs @@ -191,14 +191,14 @@ public IEnumerable Streams /// Add new stream. /// /// Endpoint stream to add. - public void AddStream(Stream stream) + public virtual void AddStream(Stream stream) { this.streams.TryAdd(stream.StreamName, stream); } } /// - /// Represents a simple TCP source endpoint providing remoted data streams. + /// Represents a simple TCP source endpoint providing a single remoted data stream. /// public class TcpSourceEndpoint : Endpoint { @@ -207,9 +207,9 @@ public class TcpSourceEndpoint : Endpoint /// /// Host name used by the endpoint. /// Port number used by the endpoint. - /// Endpoint streams. - public TcpSourceEndpoint(string host, int port, IEnumerable streams) - : base(streams) + /// Endpoint stream. + public TcpSourceEndpoint(string host, int port, Stream stream = null) + : base(stream is null ? Enumerable.Empty() : new[] { stream }) { if (string.IsNullOrEmpty(host)) { @@ -220,16 +220,6 @@ public TcpSourceEndpoint(string host, int port, IEnumerable streams) this.Port = port; } - /// - /// Initializes a new instance of the class. - /// - /// Host name used by the endpoint. - /// Port number used by the endpoint. - public TcpSourceEndpoint(string host, int port) - : this(host, port, Enumerable.Empty()) - { - } - /// /// Gets the endpoint address. /// @@ -239,6 +229,22 @@ public TcpSourceEndpoint(string host, int port) /// Gets the endpoint port number. /// public int Port { get; private set; } + + /// + /// Gets the stream (Tcp endpoints have only one). + /// + public Stream Stream => this.Streams.FirstOrDefault(); + + /// + public override void AddStream(Stream stream) + { + if (this.Streams.Count() > 0) + { + throw new InvalidOperationException($"Cannot add more than one stream to a single {nameof(TcpSourceEndpoint)}"); + } + + base.AddStream(stream); + } } /// @@ -347,6 +353,12 @@ public RemoteClockExporterEndpoint(string host, int port) /// Gets the endpoint port. /// public int Port { get; private set; } + + /// + public override void AddStream(Stream stream) + { + throw new InvalidOperationException($"Cannot add streams to a {nameof(RemoteClockExporterEndpoint)}"); + } } /// diff --git a/Sources/Runtime/Microsoft.Psi/Common/Envelope.cs b/Sources/Runtime/Microsoft.Psi/Common/Envelope.cs index 02959730b..200780e14 100644 --- a/Sources/Runtime/Microsoft.Psi/Common/Envelope.cs +++ b/Sources/Runtime/Microsoft.Psi/Common/Envelope.cs @@ -90,7 +90,7 @@ public override string ToString() /// True if the instances are equal. public override bool Equals(object other) { - if (!(other is Envelope)) + if (other is not Envelope) { return false; } diff --git a/Sources/Runtime/Microsoft.Psi/Common/Interpolators/AdjacentValuesInterpolator{TIn,TResult}.cs b/Sources/Runtime/Microsoft.Psi/Common/Interpolators/AdjacentValuesInterpolator{TIn,TResult}.cs index 007dca55c..e970f21f2 100644 --- a/Sources/Runtime/Microsoft.Psi/Common/Interpolators/AdjacentValuesInterpolator{TIn,TResult}.cs +++ b/Sources/Runtime/Microsoft.Psi/Common/Interpolators/AdjacentValuesInterpolator{TIn,TResult}.cs @@ -28,8 +28,8 @@ public class AdjacentValuesInterpolator : ReproducibleInterpolator /// /// Initializes a new instance of the class. /// - /// An interpolator function which given the two nearest values and the ratio - /// between them where the interpolation result should be produces the interpolation result. + /// A function which produces an interpolation result, given the two nearest values + /// and the ratio between them. /// Indicates whether to output a default value when no result is found. /// An optional default value to use. /// An optional name for the interpolator (defaults to AdjacentValues). diff --git a/Sources/Runtime/Microsoft.Psi/Common/Interpolators/AdjacentValuesInterpolator{T}.cs b/Sources/Runtime/Microsoft.Psi/Common/Interpolators/AdjacentValuesInterpolator{T}.cs index 6967c64e3..7d902938d 100644 --- a/Sources/Runtime/Microsoft.Psi/Common/Interpolators/AdjacentValuesInterpolator{T}.cs +++ b/Sources/Runtime/Microsoft.Psi/Common/Interpolators/AdjacentValuesInterpolator{T}.cs @@ -23,14 +23,27 @@ public class AdjacentValuesInterpolator : ReproducibleInterpolator /// /// Initializes a new instance of the class. /// - /// An interpolator function which given the two nearest values and the ratio - /// between them where the interpolation result should be produces the interpolation result. + /// A function which produces an interpolation result, given the two nearest values + /// and the ratio between them. /// Indicates whether to output a default value when no result is found. /// An optional default value to use. /// An optional name for the interpolator (defaults to AdjacentValues). public AdjacentValuesInterpolator(Func interpolatorFunc, bool orDefault, T defaultValue = default, string name = null) { - this.interpolator = new (interpolatorFunc, orDefault, defaultValue, name); + T InterpolateWithEndpointHandling(T t1, T t2, double amount) + { + if (amount < 0 || amount > 1) + { + throw new ArgumentException("Cannot pass interpolation values less than 0 or greater than 1." + + $"{nameof(AdjacentValuesInterpolator)} does not support extrapolation."); + } + + return amount == 0 ? t1 : + amount == 1 ? t2 : + interpolatorFunc(t1, t2, amount); + } + + this.interpolator = new (InterpolateWithEndpointHandling, orDefault, defaultValue, name); } /// diff --git a/Sources/Runtime/Microsoft.Psi/Common/KeyedSharedPool.cs b/Sources/Runtime/Microsoft.Psi/Common/KeyedSharedPool.cs index ff9e7bef3..60c43333f 100644 --- a/Sources/Runtime/Microsoft.Psi/Common/KeyedSharedPool.cs +++ b/Sources/Runtime/Microsoft.Psi/Common/KeyedSharedPool.cs @@ -5,6 +5,7 @@ namespace Microsoft.Psi { using System; using System.Collections.Concurrent; + using System.Linq; /// /// Provides a pool of shared objects organized by a key. The key is used both to group @@ -15,7 +16,7 @@ namespace Microsoft.Psi public class KeyedSharedPool : IDisposable where T : class { - private readonly ConcurrentDictionary> sharedPools = new ConcurrentDictionary>(); + private readonly ConcurrentDictionary> sharedPools = new (); private readonly Func allocator; private readonly int initialSize; @@ -30,6 +31,26 @@ public KeyedSharedPool(Func allocator, int initialSize = 10) this.initialSize = initialSize; } + /// + /// Resets the keyed shared pool. + /// + /// Indicates whether to clear any live objects. + /// + /// If the clearLiveObjects flag is false, an exception is thrown if a reset is attempted while the pool + /// still contains live objects. + /// + public void Reset(bool clearLiveObjects = false) + { + // Reset the individual shared pools + foreach (var pool in this.sharedPools.Values) + { + pool.Reset(clearLiveObjects); + } + + // And remove them + this.sharedPools.Clear(); + } + /// /// Get or creates a shared object from the pool. /// @@ -49,6 +70,10 @@ public void Dispose() } } + /// + public override string ToString() + => $"{this.sharedPools.Count} keys, {this.sharedPools.Sum(kvp => kvp.Value.TotalCount)} objects."; + private SharedPool GetSharedPool(TKey key) { return this.sharedPools.GetOrAdd(key, new SharedPool(() => this.allocator(key), this.initialSize)); diff --git a/Sources/Runtime/Microsoft.Psi/Common/Metadata.cs b/Sources/Runtime/Microsoft.Psi/Common/Metadata.cs index 178127a3e..73654f1da 100644 --- a/Sources/Runtime/Microsoft.Psi/Common/Metadata.cs +++ b/Sources/Runtime/Microsoft.Psi/Common/Metadata.cs @@ -3,6 +3,7 @@ namespace Microsoft.Psi { + using System; using Microsoft.Psi.Common; using Microsoft.Psi.Serialization; @@ -30,112 +31,118 @@ public enum MetadataKind : ushort /// /// Represents common metadata used in Psi stores. /// - public class Metadata + public abstract class Metadata { - internal Metadata(MetadataKind kind, string name, int id, string typeName, int version, string serializerTypeName, int serializerVersion, ushort customFlags) + internal Metadata(MetadataKind kind, string name, int id, int version, int serializationSystemVersion) { this.Kind = kind; this.Name = name; this.Id = id; - this.TypeName = typeName; this.Version = version; - this.SerializerTypeName = serializerTypeName; - this.SerializerVersion = serializerVersion; - this.CustomFlags = customFlags; + this.SerializationSystemVersion = serializationSystemVersion; } - internal Metadata() - { - } - - /// - /// Gets or sets the name of the object the metadata represents. - /// - public string Name { get; protected set; } - /// - /// Gets or sets the id of the object the metadata represents. + /// Gets the name of the object the metadata represents. /// - public int Id { get; protected set; } + public string Name { get; } /// - /// Gets or sets the name of the type of data contained in the object the metadata represents. + /// Gets the id of the object the metadata represents. /// - public string TypeName { get; protected set; } + public int Id { get; } /// - /// Gets or sets the metadata serializer type name. - /// - public string SerializerTypeName { get; protected set; } - - /// - /// Gets or sets the metadata version number. + /// Gets or sets the version number. /// public int Version { get; protected set; } /// - /// Gets or sets the metadata serializer version number. + /// Gets the serialization system version number. /// - public int SerializerVersion { get; protected set; } + public int SerializationSystemVersion { get; } /// - /// Gets or sets the metadata kind. + /// Gets the metadata kind. /// /// - public MetadataKind Kind { get; protected set; } - - /// - /// Gets or sets custom flags implemented in derived types. - /// - public ushort CustomFlags { get; protected set; } - - // custom deserializer with no dependency on the Serializer subsystem - // order of fields is important for backwards compat and must be the same as the order in Serialize, don't change! - // This method is static because the entry type differentiator is not the first field, and we need to read several - // fields before we can decide what type to create. This is for legacy reasons, since the early versions of the - // catalog file only contained one type of entry (stream metadata). + public MetadataKind Kind { get; } + + // The Metadata deserializer has no dependency on the Serializer subsystem. + // + // For legacy reasons, the metadata is manually persisted with the + // following fields and following semantics, in the order below (do not change!) + // string -> name of the metadata, meaning: + // - the stream name for PsiStreamMetadata + // - the schema name for TypeSchema + // - the assembly name for the runtime for RuntimeInfo + // int -> id of the metadata + // - the stream id for PsiStreamMetadata + // - the schema id for TypeSchema + // - N/A (0) for RuntimeInfo + // string -> type name + // - the stream type for PsiStreamMetadata + // - the data type represented by the schema for TypeSchema + // - N/A (null) for RuntimeInfo + // int -> version + // - the metadata version for PsiStreamMetadata + // - the schema version for TypeSchema + // - the runtime assembly version (major << 16 | minor) for RuntimeInfo + // string -> serializer type assembly qualified name + // - N/A (null) for PsiStreamMetadata + // - the serializer type assembly qualified name (for TypeSchema) + // - N/A (null) for RuntimeInfo + // int -> serialization system version (for PsiStreamMetadata, TypeSchema and RuntimeInfo) + // ushort -> stream metadata flags + // - stream metadata flags for PsiStreamMetadata + // - N/A (0) for TypeSchema + // - N/A (0) for RuntimeInfo + // MetadataKind -> the type of metadata (for PsiStreamMetadata, TypeSchema and RuntimeInfo) + // + // This method is static because the entry type differentiator is not the first field, + // and we need to read several fields before we can decide what type to create. This is for + // legacy reasons, since the early versions of the catalog file only contained one type of + // entry (stream metadata). + // + // Serialization happens via the overriden Deserialize method in the derived + // classes (PsiStreamMetadata, TypeSchema and RuntimeInfo). The fields described + // above are serialized in the order above, followed by fields specific to the + // derived metadata type. internal static Metadata Deserialize(BufferReader metadataBuffer) { + // Read the legacy field structure, as described above. var name = metadataBuffer.ReadString(); var id = metadataBuffer.ReadInt32(); var typeName = metadataBuffer.ReadString(); var version = metadataBuffer.ReadInt32(); var serializerTypeName = metadataBuffer.ReadString(); - var serializerVersion = metadataBuffer.ReadInt32(); + var serializationSystemVersion = metadataBuffer.ReadInt32(); var customFlags = metadataBuffer.ReadUInt16(); var kind = (MetadataKind)metadataBuffer.ReadUInt16(); if (kind == MetadataKind.StreamMetadata) { - var result = new PsiStreamMetadata(name, id, typeName, version, serializerTypeName, serializerVersion, customFlags); + var result = new PsiStreamMetadata(name, id, typeName, version, serializationSystemVersion, customFlags); result.Deserialize(metadataBuffer); return result; } else if (kind == MetadataKind.TypeSchema) { - var result = new TypeSchema(name, id, typeName, version, serializerTypeName, serializerVersion); + var result = new TypeSchema(typeName, name, id, version, serializerTypeName, serializationSystemVersion); result.Deserialize(metadataBuffer); return result; } - else + else if (kind == MetadataKind.RuntimeInfo) { - // kind == MetadataKind.RuntimeInfo - var result = new RuntimeInfo(name, id, typeName, version, serializerTypeName, serializerVersion); + var result = new RuntimeInfo(name, version, serializationSystemVersion); return result; } + else + { + throw new NotSupportedException($"Unknown metadata kind: {kind}"); + } } - // this must be called first by derived classes, before writing any of their own fields - internal virtual void Serialize(BufferWriter metadataBuffer) - { - metadataBuffer.Write(this.Name); - metadataBuffer.Write(this.Id); - metadataBuffer.Write(this.TypeName); - metadataBuffer.Write(this.Version); - metadataBuffer.Write(this.SerializerTypeName); - metadataBuffer.Write(this.SerializerVersion); - metadataBuffer.Write(this.CustomFlags); - metadataBuffer.Write((ushort)this.Kind); - } + internal abstract void Serialize(BufferWriter metadataBuffer); } } diff --git a/Sources/Runtime/Microsoft.Psi/Common/PsiStreamMetadata.cs b/Sources/Runtime/Microsoft.Psi/Common/PsiStreamMetadata.cs index fea6ab4e8..ad981123d 100644 --- a/Sources/Runtime/Microsoft.Psi/Common/PsiStreamMetadata.cs +++ b/Sources/Runtime/Microsoft.Psi/Common/PsiStreamMetadata.cs @@ -11,7 +11,6 @@ namespace Microsoft.Psi /// /// Specifies custom flags for Psi data streams. /// - /// public enum StreamMetadataFlags : ushort { /// @@ -40,22 +39,31 @@ public enum StreamMetadataFlags : ushort /// public sealed class PsiStreamMetadata : Metadata, IStreamMetadata { - private const int CurrentVersion = 2; + private const int LatestVersion = 2; private byte[] supplementalMetadataBytes = Array.Empty(); - internal PsiStreamMetadata(string name, int id, string typeName) - : base(MetadataKind.StreamMetadata, name, id, typeName, CurrentVersion, null, 0, 0) + internal PsiStreamMetadata( + string name, + int id, + string typeName, + int version = LatestVersion, + int serializationSystemVersion = RuntimeInfo.LatestSerializationSystemVersion, + ushort customFlags = 0) + : base (MetadataKind.StreamMetadata, name, id, version, serializationSystemVersion) { + this.TypeName = typeName; + this.CustomFlags = customFlags; } - internal PsiStreamMetadata(string name, int id, string typeName, int version, string serializerTypeName, int serializerVersion, ushort customFlags) - : base(MetadataKind.StreamMetadata, name, id, typeName, version, serializerTypeName, serializerVersion, customFlags) - { - } + /// + /// Gets the name of the type of data contained in the stream. + /// + public string TypeName { get; } - internal PsiStreamMetadata() - { - } + /// + /// Gets the custom flags implemented in derived types. + /// + public ushort CustomFlags { get; internal set; } /// /// Gets the time when the stream was opened. @@ -159,7 +167,7 @@ internal set /// /// Gets the time interval this stream was in existence (from open to close). /// - public TimeInterval StreamTimeInterval => new TimeInterval(this.OpenedTime, this.ClosedTime); + public TimeInterval StreamTimeInterval => new (this.OpenedTime, this.ClosedTime); /// /// Gets the interval between the creation times of the first and last messages written to this stream. @@ -307,12 +315,22 @@ internal new void Deserialize(BufferReader metadataBuffer) metadataBuffer.Read(this.supplementalMetadataBytes, len); } - this.Version = CurrentVersion; // upgrade to current version format + this.Version = LatestVersion; // upgrade to current version format } internal override void Serialize(BufferWriter metadataBuffer) { - base.Serialize(metadataBuffer); + // Serialization follows a legacy pattern of fields, as described + // in the comments at the top of the Metadata.Deserialize method. + metadataBuffer.Write(this.Name); + metadataBuffer.Write(this.Id); + metadataBuffer.Write(this.TypeName); + metadataBuffer.Write(this.Version); + metadataBuffer.Write(default(string)); // this metadata field is not used by PsiStreamMetadata + metadataBuffer.Write(this.SerializationSystemVersion); + metadataBuffer.Write(this.CustomFlags); + metadataBuffer.Write((ushort)this.Kind); + metadataBuffer.Write(this.OpenedTime); metadataBuffer.Write(this.ClosedTime); metadataBuffer.Write(this.MessageCount); diff --git a/Sources/Runtime/Microsoft.Psi/Common/RuntimeInfo.cs b/Sources/Runtime/Microsoft.Psi/Common/RuntimeInfo.cs index 4b3007c9d..c6e8fa983 100644 --- a/Sources/Runtime/Microsoft.Psi/Common/RuntimeInfo.cs +++ b/Sources/Runtime/Microsoft.Psi/Common/RuntimeInfo.cs @@ -12,9 +12,9 @@ namespace Microsoft.Psi.Common public class RuntimeInfo : Metadata { /// - /// The current version of the serialization subsystem. This is the default. + /// The latest version of the serialization subsystem. /// - public const int CurrentRuntimeVersion = 2; + public const int LatestSerializationSystemVersion = 2; /// /// Gets name of the executing assembly. @@ -22,24 +22,35 @@ public class RuntimeInfo : Metadata public static readonly AssemblyName RuntimeName = Assembly.GetExecutingAssembly().GetName(); /// - /// Gets the current runtime info. + /// Gets the latest (current) runtime info. /// - public static readonly RuntimeInfo Current = new (); + public static readonly RuntimeInfo Latest = new (); - internal RuntimeInfo(int serializationSystemVersion = CurrentRuntimeVersion) + internal RuntimeInfo(int serializationSystemVersion = LatestSerializationSystemVersion) : this( - name: RuntimeName.FullName, - id: 0, - typeName: default(string), + RuntimeName.FullName, version: (RuntimeName.Version.Major << 16) | RuntimeName.Version.Minor, - serializerTypeName: default(string), - serializerVersion: serializationSystemVersion) + serializationSystemVersion: serializationSystemVersion) { } - internal RuntimeInfo(string name, int id, string typeName, int version, string serializerTypeName, int serializerVersion) - : base(MetadataKind.RuntimeInfo, name, id, typeName, version, serializerTypeName, serializerVersion, 0) + internal RuntimeInfo(string name, int version, int serializationSystemVersion) + : base (MetadataKind.RuntimeInfo, name, 0, version, serializationSystemVersion) { } + + internal override void Serialize(BufferWriter metadataBuffer) + { + // Serialization follows a legacy pattern of fields, as described + // in the comments at the top of the Metadata.Deserialize method. + metadataBuffer.Write(this.Name); + metadataBuffer.Write(this.Id); + metadataBuffer.Write(default(string)); // this metadata field is not used by RuntimeInfo + metadataBuffer.Write(this.Version); + metadataBuffer.Write(default(string)); // this metadata field is not used by RuntimeInfo + metadataBuffer.Write(this.SerializationSystemVersion); + metadataBuffer.Write(default(ushort)); // this metadata field is not used by RuntimeInfo + metadataBuffer.Write((ushort)this.Kind); + } } } diff --git a/Sources/Runtime/Microsoft.Psi/Common/Shared.cs b/Sources/Runtime/Microsoft.Psi/Common/Shared.cs index 05149b3e6..74d4eeb83 100644 --- a/Sources/Runtime/Microsoft.Psi/Common/Shared.cs +++ b/Sources/Runtime/Microsoft.Psi/Common/Shared.cs @@ -208,7 +208,7 @@ public string ToString(string format, IFormatProvider formatProvider) // This is done to avoid keeping a reference to an object that is still in use. private class CustomSerializer : ISerializer> { - public const int Version = 2; + public const int LatestSchemaVersion = 2; private SerializationHandler> handler; /// @@ -218,9 +218,17 @@ public TypeSchema Initialize(KnownSerializers serializers, TypeSchema targetSche { this.handler = serializers.GetHandler>(); var type = typeof(Shared); - var name = TypeSchema.GetContractName(type, serializers.RuntimeVersion); + var name = TypeSchema.GetContractName(type, serializers.RuntimeInfo.SerializationSystemVersion); var innerMember = new TypeMemberSchema("inner", typeof(SharedContainer).AssemblyQualifiedName, true); - var schema = new TypeSchema(name, TypeSchema.GetId(name), type.AssemblyQualifiedName, TypeFlags.IsClass, new TypeMemberSchema[] { innerMember }, Version); + var schema = new TypeSchema( + type.AssemblyQualifiedName, + TypeFlags.IsClass, + new TypeMemberSchema[] { innerMember }, + name, + TypeSchema.GetId(name), + LatestSchemaVersion, + this.GetType().AssemblyQualifiedName, + serializers.RuntimeInfo.SerializationSystemVersion); return targetSchema ?? schema; } diff --git a/Sources/Runtime/Microsoft.Psi/Common/SharedContainer.cs b/Sources/Runtime/Microsoft.Psi/Common/SharedContainer.cs index 6ac10201c..240cc6211 100644 --- a/Sources/Runtime/Microsoft.Psi/Common/SharedContainer.cs +++ b/Sources/Runtime/Microsoft.Psi/Common/SharedContainer.cs @@ -35,7 +35,7 @@ internal SharedContainer(T resource, SharedPool pool) public void AddRef() { - var newVal = Interlocked.Increment(ref this.refCount); + Interlocked.Increment(ref this.refCount); } public void Release() @@ -55,9 +55,9 @@ public void Release() } else { - if (this.resource is IDisposable) + if (this.resource is IDisposable disposable) { - ((IDisposable)this.resource).Dispose(); + disposable.Dispose(); } } @@ -71,7 +71,7 @@ public void Release() private class CustomSerializer : ISerializer> { - public const int Version = 2; + public const int LatestSchemaVersion = 2; private SerializationHandler handler; /// @@ -81,9 +81,17 @@ public TypeSchema Initialize(KnownSerializers serializers, TypeSchema targetSche { this.handler = serializers.GetHandler(); var type = typeof(SharedContainer); - var name = TypeSchema.GetContractName(type, serializers.RuntimeVersion); + var name = TypeSchema.GetContractName(type, serializers.RuntimeInfo.SerializationSystemVersion); var resourceMember = new TypeMemberSchema("resource", typeof(T).AssemblyQualifiedName, true); - var schema = new TypeSchema(name, TypeSchema.GetId(name), type.AssemblyQualifiedName, TypeFlags.IsClass, new TypeMemberSchema[] { resourceMember }, Version); + var schema = new TypeSchema( + type.AssemblyQualifiedName, + TypeFlags.IsClass, + new TypeMemberSchema[] { resourceMember }, + name, + TypeSchema.GetId(name), + LatestSchemaVersion, + this.GetType().AssemblyQualifiedName, + serializers.RuntimeInfo.SerializationSystemVersion); return targetSchema ?? schema; } @@ -113,7 +121,7 @@ public void Clone(SharedContainer instance, ref SharedContainer target, Se public void PrepareDeserializationTarget(BufferReader reader, ref SharedContainer target, SerializationContext context) { SharedPool sharedPool = null; - T resource = default(T); + var resource = default(T); if (target != null) { diff --git a/Sources/Runtime/Microsoft.Psi/Common/SharedPool.cs b/Sources/Runtime/Microsoft.Psi/Common/SharedPool.cs index 21d254521..fcea27e97 100644 --- a/Sources/Runtime/Microsoft.Psi/Common/SharedPool.cs +++ b/Sources/Runtime/Microsoft.Psi/Common/SharedPool.cs @@ -15,10 +15,10 @@ namespace Microsoft.Psi public class SharedPool : IDisposable where T : class { - private readonly List keepAlive; private readonly Func allocator; private readonly KnownSerializers serializers; - private readonly object availableLock = new object(); + private readonly object availableLock = new (); + private List pool; private Queue available; /// @@ -31,12 +31,12 @@ public SharedPool(Func allocator, int initialSize = 10, KnownSerializers know { this.allocator = allocator; this.available = new Queue(initialSize); - this.keepAlive = new List(); + this.pool = new List(); this.serializers = knownSerializers; } /// - /// Gets the number of objects available in the pool. + /// Gets the number of objects available, i.e., that are not live, in the pool. /// public int AvailableCount { @@ -44,7 +44,7 @@ public int AvailableCount { lock (this.availableLock) { - return this.available.Count; + return this.available != null ? this.available.Count : 0; } } } @@ -56,9 +56,47 @@ public int TotalCount { get { - lock (this.keepAlive) + lock (this.pool) { - return this.keepAlive.Count; + return this.pool.Count; + } + } + } + + /// + /// Resets the shared pool. + /// + /// Indicates whether to clear any live objects. + /// + /// If the clearLiveObjects flag is false, an exception is thrown if a reset is attempted while the pool + /// still contains live objects. + /// + public void Reset(bool clearLiveObjects = false) + { + lock (this.availableLock) + { + lock (this.pool) + { + // If no object is still alive, then reset the pool + if (clearLiveObjects || (this.available.Count == this.pool.Count)) + { + // Dispose all the objects in the pool + if (typeof(IDisposable).IsAssignableFrom(typeof(T))) + { + foreach (var entry in this.available) + { + ((IDisposable)entry).Dispose(); + } + } + + // Re-initialize + this.available = new (); + this.pool = new (); + } + else + { + throw new InvalidOperationException("Cannot reset a shared pool that contains live objects."); + } } } } @@ -109,9 +147,9 @@ public Shared GetOrCreate() if (!this.TryGet(out T recycled)) { recycled = this.allocator(); - lock (this.keepAlive) + lock (this.pool) { - this.keepAlive.Add(recycled); + this.pool.Add(recycled); } } @@ -155,9 +193,9 @@ internal void Recycle(T recyclable) else { // dispose the recycled object if it is disposable - if (recyclable is IDisposable) + if (recyclable is IDisposable disposable) { - ((IDisposable)recyclable).Dispose(); + disposable.Dispose(); } } } diff --git a/Sources/Runtime/Microsoft.Psi/Common/TypeResolutionHelper.cs b/Sources/Runtime/Microsoft.Psi/Common/TypeResolutionHelper.cs index 14cda5590..25f2ae33f 100644 --- a/Sources/Runtime/Microsoft.Psi/Common/TypeResolutionHelper.cs +++ b/Sources/Runtime/Microsoft.Psi/Common/TypeResolutionHelper.cs @@ -4,6 +4,8 @@ namespace Microsoft.Psi { using System; + using System.Collections.Concurrent; + using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text.RegularExpressions; @@ -13,6 +15,8 @@ namespace Microsoft.Psi /// public static class TypeResolutionHelper { + private static readonly ConcurrentDictionary TypeCache = new (); + /// /// Gets a type by its type name. This method will only return types from loaded /// assemblies, i.e. assemblies explicitly referenced or loaded by this application. @@ -21,6 +25,11 @@ public static class TypeResolutionHelper /// The requested type, or null if the type was not found. public static Type GetVerifiedType(string typeName) { + if (TypeCache.ContainsKey(typeName)) + { + return TypeCache[typeName]; + } + var type = Type.GetType(typeName, AssemblyResolver, null); if (type == null) @@ -33,6 +42,11 @@ public static Type GetVerifiedType(string typeName) type = Type.GetType(typeName, AssemblyResolver, null); } + if (type != null) + { + TypeCache.TryAdd(typeName, type); + } + return type; } diff --git a/Sources/Runtime/Microsoft.Psi/Common/UnmanagedArray.cs b/Sources/Runtime/Microsoft.Psi/Common/UnmanagedArray.cs index 4852e48f6..beb7a397f 100644 --- a/Sources/Runtime/Microsoft.Psi/Common/UnmanagedArray.cs +++ b/Sources/Runtime/Microsoft.Psi/Common/UnmanagedArray.cs @@ -26,10 +26,10 @@ public unsafe class UnmanagedArray : IList, IDisposable /// public static readonly int ElementSize = BufferEx.SizeOf(); + private readonly bool isReadOnly; private IntPtr data; private int length; private bool ownsMemory; - private bool isReadOnly; /// /// Initializes a new instance of the class from an existing allocation. @@ -312,7 +312,7 @@ public void CopyTo(UnmanagedArray destination) /// The index in the destination array at which copying begins. public void CopyTo(UnmanagedArray destination, int index) { - this.CopyTo(destination, 0, 0, this.length); + this.CopyTo(destination, 0, index, this.length); } /// @@ -525,7 +525,7 @@ public void Reset() // serializer compatible with T[] private class CustomSerializer : ISerializer> { - private const int Version = 1; + private const int LatestSchemaVersion = 1; /// public bool? IsClearRequired => false; @@ -535,9 +535,17 @@ public TypeSchema Initialize(KnownSerializers serializers, TypeSchema targetSche serializers.GetHandler(); // register element type var type = typeof(T[]); - var name = TypeSchema.GetContractName(type, serializers.RuntimeVersion); + var name = TypeSchema.GetContractName(type, serializers.RuntimeInfo.SerializationSystemVersion); var elementsMember = new TypeMemberSchema("Elements", typeof(T).AssemblyQualifiedName, true); - var schema = new TypeSchema(name, TypeSchema.GetId(name), type.AssemblyQualifiedName, TypeFlags.IsCollection, new TypeMemberSchema[] { elementsMember }, Version); + var schema = new TypeSchema( + type.AssemblyQualifiedName, + TypeFlags.IsCollection, + new TypeMemberSchema[] { elementsMember }, + name, + TypeSchema.GetId(name), + LatestSchemaVersion, + this.GetType().AssemblyQualifiedName, + serializers.RuntimeInfo.SerializationSystemVersion); return targetSchema ?? schema; } diff --git a/Sources/Runtime/Microsoft.Psi/Common/UnmanagedBuffer.cs b/Sources/Runtime/Microsoft.Psi/Common/UnmanagedBuffer.cs index 5a8fc09f4..e785ea239 100644 --- a/Sources/Runtime/Microsoft.Psi/Common/UnmanagedBuffer.cs +++ b/Sources/Runtime/Microsoft.Psi/Common/UnmanagedBuffer.cs @@ -120,15 +120,20 @@ public UnmanagedBuffer Clone() /// Copy this unmanaged buffer to another instance. /// /// Destination instance to which to copy. - public void CopyTo(UnmanagedBuffer destination) + /// Size (bytes) to copy. + public void CopyTo(UnmanagedBuffer destination, int size) { if (destination == null) { - throw new ArgumentException("Destination unmanaged array is null."); + throw new ArgumentException("Destination unmanaged buffer is null."); + } + else if (this.size < size) + { + throw new ArgumentException("Source unmanaged buffer is not of sufficient size."); } - else if (destination.size != this.size) + else if (destination.Size < size) { - throw new ArgumentException("Destination unmanaged array is not of the same size."); + throw new ArgumentException("Destination unmanaged buffer is not of sufficient size."); } else { @@ -144,6 +149,11 @@ public void CopyTo(UnmanagedBuffer destination) /// Bytes having been copied. public byte[] ReadBytes(int count, int offset = 0) { + if (this.size < count + offset) + { + throw new ArgumentException("Unmanaged buffer is not of sufficient size."); + } + var result = new byte[count]; Marshal.Copy(IntPtr.Add(this.data, offset), result, 0, count); return result; @@ -153,18 +163,23 @@ public byte[] ReadBytes(int count, int offset = 0) /// Copy unmanaged buffer to managed array. /// /// Destination array to which to copy. - public void CopyTo(byte[] destination) + /// Size (bytes) to copy. + public void CopyTo(byte[] destination, int size) { if (destination == null) { - throw new ArgumentException("Destination buffer is null."); + throw new ArgumentException("Destination array is null."); } - else if (this.size != destination.Length) + else if (this.size < size) { - throw new ArgumentException("Destination buffer is not of the same size."); + throw new ArgumentException("Source unmanaged buffer is not of sufficient size."); + } + else if (destination.Length < size) + { + throw new ArgumentException("Destination array is not of sufficient size."); } - Marshal.Copy(this.data, destination, 0, destination.Length); + Marshal.Copy(this.data, destination, 0, size); } /// @@ -174,30 +189,35 @@ public void CopyTo(byte[] destination) /// Size (bytes) to copy. public void CopyTo(IntPtr destination, int size) { - if (size != this.size) + if (this.size < size) { - throw new ArgumentException("Destination size is not the same as source."); + throw new ArgumentException("Source unmanaged buffer is not of sufficient size."); } - CopyUnmanagedMemory(destination, this.data, this.size); + CopyUnmanagedMemory(destination, this.data, size); } /// /// Copy from unmanaged buffer. /// /// Unmanaged buffer from which to copy. - public void CopyFrom(UnmanagedBuffer source) + /// Size (bytes) to copy. + public void CopyFrom(UnmanagedBuffer source, int size) { if (source == null) { throw new ArgumentException("Source unmanaged array is null."); } - else if (this.size != source.Size) + else if (this.size < size) { - throw new ArgumentException("Source unmanaged array is not of the same size."); + throw new ArgumentException("Destination unmanaged array is not of sufficient size."); + } + else if (source.Size < size) + { + throw new ArgumentException("Source unmanaged array is not of sufficient size."); } - CopyUnmanagedMemory(this.data, source.data, this.size); + CopyUnmanagedMemory(this.data, source.data, size); } /// @@ -219,11 +239,15 @@ public void CopyFrom(byte[] source, int offset, int length) { if (source == null) { - throw new ArgumentException("Source buffer is null."); + throw new ArgumentException("Source array is null."); + } + else if (this.size < length) + { + throw new ArgumentException("Destination unmanaged buffer is not of sufficient size."); } - else if (this.size != length) + else if (source.Length < offset + length) { - throw new ArgumentException("Source buffer is not of the same size."); + throw new ArgumentException("Source array is not of sufficient size."); } Marshal.Copy(source, offset, this.data, length); @@ -236,12 +260,12 @@ public void CopyFrom(byte[] source, int offset, int length) /// Size (bytes) to copy. public void CopyFrom(IntPtr source, int size) { - if (size != this.size) + if (this.size < size) { - throw new ArgumentException("Destination size is not the same as source."); + throw new ArgumentException("Destination unmanaged buffer is not of sufficient size."); } - CopyUnmanagedMemory(this.data, source, this.size); + CopyUnmanagedMemory(this.data, source, size); } /// @@ -272,7 +296,7 @@ private void DisposeUnmanaged() private class CustomSerializer : ISerializer { - public const int Version = 2; + public const int LatestSchemaVersion = 2; /// public bool? IsClearRequired => false; @@ -281,9 +305,17 @@ public TypeSchema Initialize(KnownSerializers serializers, TypeSchema targetSche { serializers.GetHandler(); // register element type var type = typeof(byte[]); - var name = TypeSchema.GetContractName(type, serializers.RuntimeVersion); + var name = TypeSchema.GetContractName(type, serializers.RuntimeInfo.SerializationSystemVersion); var elementsMember = new TypeMemberSchema("Elements", typeof(byte).AssemblyQualifiedName, true); - var schema = new TypeSchema(name, TypeSchema.GetId(name), type.AssemblyQualifiedName, TypeFlags.IsCollection, new TypeMemberSchema[] { elementsMember }, Version); + var schema = new TypeSchema( + type.AssemblyQualifiedName, + TypeFlags.IsCollection, + new TypeMemberSchema[] { elementsMember }, + name, + TypeSchema.GetId(name), + LatestSchemaVersion, + this.GetType().AssemblyQualifiedName, + serializers.RuntimeInfo.SerializationSystemVersion); return targetSchema ?? schema; } diff --git a/Sources/Runtime/Microsoft.Psi/Components/Generator.cs b/Sources/Runtime/Microsoft.Psi/Components/Generator.cs index 7b2fc0f7b..29a16f638 100644 --- a/Sources/Runtime/Microsoft.Psi/Components/Generator.cs +++ b/Sources/Runtime/Microsoft.Psi/Components/Generator.cs @@ -91,6 +91,13 @@ public void Start(Action notifyCompletionTime) /// public void Stop(DateTime finalOriginatingTime, Action notifyCompleted) { + // If the generator has already stopped, call notify completed. + if (this.stopped) + { + notifyCompleted.Invoke(); + return; + } + this.finalMessageTime = finalOriginatingTime; this.notifyCompleted = notifyCompleted; diff --git a/Sources/Runtime/Microsoft.Psi/Data/Exporter.cs b/Sources/Runtime/Microsoft.Psi/Data/Exporter.cs index e17ef56bf..aba6f353c 100644 --- a/Sources/Runtime/Microsoft.Psi/Data/Exporter.cs +++ b/Sources/Runtime/Microsoft.Psi/Data/Exporter.cs @@ -50,7 +50,7 @@ protected internal Exporter(Pipeline pipeline, string name, string path, bool cr this.pipeline.PipelineRun += (_, e) => this.writer.InitializeStreamOpenedTimes(e.StartOriginatingTime); // write the version info - this.writer.WriteToCatalog(this.serializers.RuntimeVersion); + this.writer.WriteToCatalog(this.serializers.RuntimeInfo); // copy the schemas present so far and also make sure the catalog captures schemas added in the future this.serializers.SchemaAdded += (o, e) => this.writer.WriteToCatalog(e); diff --git a/Sources/Runtime/Microsoft.Psi/Data/PsiStore.cs b/Sources/Runtime/Microsoft.Psi/Data/PsiStore.cs index 259e0fa20..c91e30985 100644 --- a/Sources/Runtime/Microsoft.Psi/Data/PsiStore.cs +++ b/Sources/Runtime/Microsoft.Psi/Data/PsiStore.cs @@ -490,7 +490,7 @@ public static string GetPathToLatestVersion(string storeName, string rootPath) { if (!PsiStoreCommon.TryGetPathToLatestVersion(storeName, rootPath, out string path)) { - throw new InvalidOperationException($"No matching files found: {rootPath} \\[{storeName}.*\\]{storeName}*"); + throw new InvalidOperationException($"No matching files found for store \"{storeName}\" at path \"{rootPath}\""); } return path; @@ -512,7 +512,7 @@ public static IEnumerable<(string Name, string Path)> EnumerateStores(string roo /// /// The name and path of the store to delete. /// Whether to delete the containing directory if it is empty after removing store files. - internal static void Delete((string Name, string Path) store, bool deleteDirectoryIfOtherwiseEmpty = false) + public static void Delete((string Name, string Path) store, bool deleteDirectoryIfOtherwiseEmpty = false) { foreach (var fileInfo in PsiStoreCommon.EnumerateStoreFiles(store.Name, store.Path)) { diff --git a/Sources/Runtime/Microsoft.Psi/Data/PsiStoreStreamReader.cs b/Sources/Runtime/Microsoft.Psi/Data/PsiStoreStreamReader.cs index 57893499d..882c909fb 100644 --- a/Sources/Runtime/Microsoft.Psi/Data/PsiStoreStreamReader.cs +++ b/Sources/Runtime/Microsoft.Psi/Data/PsiStoreStreamReader.cs @@ -255,13 +255,10 @@ private T Read(IndexEntry indexEntry, Func allocator = null) /// Initializes the serialization subsystem with the metadata from the store. /// /// The collection of metadata entries from the store catalog. - /// The version of the runtime that produced the store. - private void LoadMetadata(IEnumerable metadata, RuntimeInfo runtimeVersion) + /// The runtime info for the runtime that produced the store. + private void LoadMetadata(IEnumerable metadata, RuntimeInfo runtimeInfo) { - if (this.context == null) - { - this.context = new SerializationContext(new KnownSerializers(runtimeVersion)); - } + this.context ??= new SerializationContext(new KnownSerializers(runtimeInfo)); this.context.Serializers.RegisterMetadata(metadata); } @@ -367,7 +364,7 @@ private T Deserialize(SerializationHandler handler, BufferReader br, Envel { if (isDynamic) { - var deserializer = new DynamicMessageDeserializer(typeName, schemas); + var deserializer = new DynamicMessageDeserializer(typeName, schemas, this.context.Serializers.TypeNameSynonyms); objectToReuse = deserializer.Deserialize(br); } else if (isRaw) diff --git a/Sources/Runtime/Microsoft.Psi/Diagnostics/PipelineDiagnostics.cs b/Sources/Runtime/Microsoft.Psi/Diagnostics/PipelineDiagnostics.cs index 325873527..77d1801f6 100644 --- a/Sources/Runtime/Microsoft.Psi/Diagnostics/PipelineDiagnostics.cs +++ b/Sources/Runtime/Microsoft.Psi/Diagnostics/PipelineDiagnostics.cs @@ -520,6 +520,32 @@ internal ReceiverDiagnostics(PipelineDiagnosticsInternal.ReceiverDiagnostics rec } } + /// + /// Gets the name of all the statistics. + /// + public static string[] AllStatistics => new string[] + { + nameof(AvgMessageEmittedLatency), + nameof(AvgMessageCreatedLatency), + nameof(AvgMessageProcessTime), + nameof(AvgMessageReceivedLatency), + nameof(AvgMessageSize), + nameof(AvgDeliveryQueueSize), + nameof(LastMessageEmittedLatency), + nameof(LastMessageCreatedLatency), + nameof(LastMessageProcessTime), + nameof(LastMessageReceivedLatency), + nameof(LastMessageSize), + nameof(LastDeliveryQueueSize), + nameof(ReceiverIsThrottled), + nameof(TotalMessageDroppedCount), + nameof(TotalMessageEmittedCount), + nameof(TotalMessageProcessedCount), + nameof(WindowMessageDroppedCount), + nameof(WindowMessageEmittedCount), + nameof(WindowMessageProcessedCount), + }; + /// /// Gets receiver ID. /// diff --git a/Sources/Runtime/Microsoft.Psi/Executive/DebugExtensions.cs b/Sources/Runtime/Microsoft.Psi/Executive/DebugExtensions.cs index 72598bcd3..90a0aecae 100644 --- a/Sources/Runtime/Microsoft.Psi/Executive/DebugExtensions.cs +++ b/Sources/Runtime/Microsoft.Psi/Executive/DebugExtensions.cs @@ -143,7 +143,7 @@ public static void DumpStructure(this Pipeline pipeline, string fileName) { if (receiver.Value.Source != null) { - var emitterComp = pipeline.Components.First(c => c.Outputs.ContainsValue(receiver.Value.Source)); + var emitterComp = pipeline.Components.First(c => c.Outputs.Values.Contains(receiver.Value.Source)); var emitComponentName = System.Security.SecurityElement.Escape(emitterComp.Name); var emitterName = System.Security.SecurityElement.Escape(pipeline.Components.SelectMany(c => c.Outputs).First(e => e.Value == receiver.Value.Source).Key); var receiverName = System.Security.SecurityElement.Escape(receiver.Key); diff --git a/Sources/Runtime/Microsoft.Psi/Executive/PipelineElement.cs b/Sources/Runtime/Microsoft.Psi/Executive/PipelineElement.cs index 950c1d4e5..03d119c6a 100644 --- a/Sources/Runtime/Microsoft.Psi/Executive/PipelineElement.cs +++ b/Sources/Runtime/Microsoft.Psi/Executive/PipelineElement.cs @@ -27,15 +27,15 @@ internal class PipelineElement private static readonly AsyncLocal ExecutionContextPipelineSlot = new AsyncLocal(); #endif - private static readonly ConcurrentDictionary Locks = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary Locks = new (); private readonly int id; + private readonly ConcurrentDictionary outputs = new (); + private readonly ConcurrentDictionary inputs = new (); + private readonly SynchronizationLock syncContext; + private readonly string name; private object stateObject; - private Dictionary outputs = new Dictionary(); - private Dictionary inputs = new Dictionary(); - private SynchronizationLock syncContext; - private string name; private Pipeline pipeline; private State state = State.Initial; private DateTime finalOriginatingTime; @@ -104,9 +104,9 @@ private enum State internal object StateObject => this.stateObject; - internal Dictionary Outputs => this.outputs; + internal ConcurrentDictionary Outputs => this.outputs; - internal Dictionary Inputs => this.inputs; + internal ConcurrentDictionary Inputs => this.inputs; internal IEnumerable InputNames => this.inputs.Keys; @@ -244,9 +244,9 @@ internal void Dispose() input.Dispose(); } - if (this.stateObject is IDisposable) + if (this.stateObject is IDisposable disposable) { - ((IDisposable)this.stateObject).Dispose(); + disposable.Dispose(); } Locks.TryRemove(this.stateObject, out var _); @@ -370,15 +370,25 @@ internal IReceiver GetInput(string name) internal void AddOutput(string name, IEmitter output) { - name = name ?? $"{this.Name}<{output.GetType().GetGenericArguments()[0].Name}> {output.GetHashCode()}"; - this.outputs.Add(name, output); + name ??= $"{this.Name}<{output.GetType().GetGenericArguments()[0].Name}> {output.GetHashCode()}"; + + if (!this.outputs.TryAdd(name, output)) + { + throw new ArgumentException($"Cannot add another output named {name} because one already exists!"); + } + this.pipeline.DiagnosticsCollector?.PipelineElementAddEmitter(this.pipeline, this, output); } internal void AddInput(string name, IReceiver input) { - name = name ?? $"{this.Name}[{input.GetType().GetGenericArguments()[0].Name}] {input.GetHashCode()}"; - this.inputs.Add(name, input); + name ??= $"{this.Name}[{input.GetType().GetGenericArguments()[0].Name}] {input.GetHashCode()}"; + + if (!this.inputs.TryAdd(name, input)) + { + throw new ArgumentException($"Cannot add another input named {name} because one already exists!"); + } + this.pipeline.DiagnosticsCollector?.PipelineElementAddReceiver(this.pipeline, this, input); } } diff --git a/Sources/Runtime/Microsoft.Psi/Persistence/InfiniteFileReader.cs b/Sources/Runtime/Microsoft.Psi/Persistence/InfiniteFileReader.cs index 89d3365b2..c8d5ee7da 100644 --- a/Sources/Runtime/Microsoft.Psi/Persistence/InfiniteFileReader.cs +++ b/Sources/Runtime/Microsoft.Psi/Persistence/InfiniteFileReader.cs @@ -232,7 +232,14 @@ private void LoadNextExtent() if (this.mappedFile == null) { // attach to an in-memory MMF - this.mappedFile = MemoryMappedFile.OpenExisting(extentName); + try + { + this.mappedFile = MemoryMappedFile.OpenExisting(extentName); + } + catch (Exception ex) + { + throw new Exception($"Failed to open extent: {extentName}.", ex); + } } this.view = this.mappedFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read); diff --git a/Sources/Runtime/Microsoft.Psi/Persistence/InfiniteFileWriter.cs b/Sources/Runtime/Microsoft.Psi/Persistence/InfiniteFileWriter.cs index c44613509..05e146e06 100644 --- a/Sources/Runtime/Microsoft.Psi/Persistence/InfiniteFileWriter.cs +++ b/Sources/Runtime/Microsoft.Psi/Persistence/InfiniteFileWriter.cs @@ -71,7 +71,11 @@ public InfiniteFileWriter(string path, string fileName, int extentSize) } catch (ObjectDisposedException) { - // ignore + // ignore if localWritePulse was disposed + } + catch (AbandonedMutexException) + { + // ignore if globalWritePulse was disposed } })) { IsBackground = true }.Start(); diff --git a/Sources/Runtime/Microsoft.Psi/Persistence/MessageWriter.cs b/Sources/Runtime/Microsoft.Psi/Persistence/MessageWriter.cs index e92ac6555..ced82f1a5 100644 --- a/Sources/Runtime/Microsoft.Psi/Persistence/MessageWriter.cs +++ b/Sources/Runtime/Microsoft.Psi/Persistence/MessageWriter.cs @@ -56,7 +56,7 @@ public int Write(BufferReader buffer, Envelope envelope) public int Write(Envelope envelope, byte[] source, int start, int count) { - // for now, lock. To get rid of it we need to split an ExtentWriter out of the InifinteFileWriter + // for now, lock. To get rid of it we need to split an ExtentWriter out of the InfiniteFileWriter lock (this.fileWriter) { unsafe diff --git a/Sources/Runtime/Microsoft.Psi/Persistence/MetadataCache.cs b/Sources/Runtime/Microsoft.Psi/Persistence/MetadataCache.cs index 65436f6cb..51e86f557 100644 --- a/Sources/Runtime/Microsoft.Psi/Persistence/MetadataCache.cs +++ b/Sources/Runtime/Microsoft.Psi/Persistence/MetadataCache.cs @@ -10,17 +10,17 @@ namespace Microsoft.Psi.Persistence internal class MetadataCache : IDisposable { - private readonly object syncRoot = new object(); + private readonly object syncRoot = new (); private readonly string name; private readonly string path; - private volatile Dictionary streamDescriptors = new Dictionary(); - private volatile Dictionary streamDescriptorsById = new Dictionary(); + private readonly Action, RuntimeInfo> entriesAdded; + private volatile Dictionary streamDescriptors = new (); + private volatile Dictionary streamDescriptorsById = new (); private InfiniteFileReader catalogReader; private TimeInterval messageCreationTimeInterval; private TimeInterval messageOriginatingTimeInterval; private TimeInterval streamTimeInterval; - private Action, RuntimeInfo> entriesAdded; - private RuntimeInfo runtimeVersion; + private RuntimeInfo runtimeInfo; public MetadataCache(string name, string path, Action, RuntimeInfo> entriesAdded) { @@ -30,11 +30,11 @@ public MetadataCache(string name, string path, Action, Run this.entriesAdded = entriesAdded; // assume v0 for backwards compat. Update will fix this up if the file is newer. - this.runtimeVersion = new RuntimeInfo(0); + this.runtimeInfo = new RuntimeInfo(0); this.Update(); } - public RuntimeInfo RuntimeVersion => this.runtimeVersion; + public RuntimeInfo RuntimeInfo => this.runtimeInfo; public IEnumerable AvailableStreams { @@ -132,7 +132,7 @@ public void Update() if (meta.Kind == MetadataKind.RuntimeInfo) { // we expect this to be first in the file (or completely missing in v0 files) - this.runtimeVersion = meta as RuntimeInfo; + this.runtimeInfo = meta as RuntimeInfo; // Need to review this. The issue was that the RemoteExporter is not writing // out the RuntimeInfo to the stream. This causes the RemoteImporter side of things to @@ -178,7 +178,7 @@ public void Update() // let the registered delegates know about the change if (newMetadata.Count > 0 && this.entriesAdded != null) { - this.entriesAdded(newMetadata, this.runtimeVersion); + this.entriesAdded(newMetadata, this.runtimeInfo); } } } diff --git a/Sources/Runtime/Microsoft.Psi/Persistence/PsiStoreReader.cs b/Sources/Runtime/Microsoft.Psi/Persistence/PsiStoreReader.cs index bee6afcdd..5cff8bfe5 100644 --- a/Sources/Runtime/Microsoft.Psi/Persistence/PsiStoreReader.cs +++ b/Sources/Runtime/Microsoft.Psi/Persistence/PsiStoreReader.cs @@ -18,12 +18,12 @@ namespace Microsoft.Psi.Persistence /// public sealed class PsiStoreReader : IDisposable { - private readonly Dictionary isIndexedStream = new Dictionary(); + private readonly Dictionary isIndexedStream = new (); private readonly MessageReader messageReader; private readonly MessageReader largeMessageReader; private readonly Shared metadataCache; private readonly Shared indexCache; - private readonly HashSet enabledStreams = new HashSet(); + private readonly HashSet enabledStreams = new (); private TimeInterval replayInterval = TimeInterval.Empty; private bool useOriginatingTime = false; @@ -111,9 +111,9 @@ public PsiStoreReader(PsiStoreReader other) public int StreamCount => this.metadataCache.Resource.AvailableStreams.Count(); /// - /// Gets the version of the runtime used to write to this store. + /// Gets info about the runtime that was used to write to this store. /// - public RuntimeInfo RuntimeVersion => this.metadataCache.Resource.RuntimeVersion; + public RuntimeInfo RuntimeInfo => this.metadataCache.Resource.RuntimeInfo; /// /// Opens the specified stream for reading. @@ -365,7 +365,11 @@ public int Read(ref byte[] buffer) var extentId = indexEntry.ExtentId - int.MinValue; this.largeMessageReader.Seek(extentId, indexEntry.Position); - this.largeMessageReader.MoveNext(); + if (!this.largeMessageReader.MoveNext()) + { + throw new ArgumentException($"Invalid index entry (extent: {extentId}, position: {indexEntry.Position}, current: {this.largeMessageReader.CurrentExtentId})"); + } + return this.largeMessageReader.Read(ref buffer); } @@ -385,12 +389,20 @@ public int ReadAt(IndexEntry indexEntry, ref byte[] buffer) { var extentId = indexEntry.ExtentId - int.MinValue; this.largeMessageReader.Seek(extentId, indexEntry.Position); - this.largeMessageReader.MoveNext(); + if (!this.largeMessageReader.MoveNext()) + { + throw new ArgumentException($"Invalid index entry (extent: {indexEntry.ExtentId - int.MinValue}, position: {indexEntry.Position}, current: {this.largeMessageReader.CurrentExtentId})"); + } + return this.largeMessageReader.Read(ref buffer); } this.messageReader.Seek(indexEntry.ExtentId, indexEntry.Position); - this.messageReader.MoveNext(); + if (!this.messageReader.MoveNext()) + { + throw new ArgumentException($"Invalid index entry (extent: {indexEntry.ExtentId}, position: {indexEntry.Position}, current: {this.messageReader.CurrentExtentId})"); + } + return this.messageReader.Read(ref buffer); } diff --git a/Sources/Runtime/Microsoft.Psi/Persistence/PsiStoreWriter.cs b/Sources/Runtime/Microsoft.Psi/Persistence/PsiStoreWriter.cs index d059310de..2b17c9a36 100644 --- a/Sources/Runtime/Microsoft.Psi/Persistence/PsiStoreWriter.cs +++ b/Sources/Runtime/Microsoft.Psi/Persistence/PsiStoreWriter.cs @@ -4,6 +4,7 @@ namespace Microsoft.Psi.Persistence { using System; + using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; @@ -38,7 +39,7 @@ public sealed class PsiStoreWriter : IDisposable private readonly InfiniteFileWriter catalogWriter; private readonly InfiniteFileWriter pageIndexWriter; private readonly MessageWriter writer; - private readonly Dictionary metadata = new (); + private readonly ConcurrentDictionary metadata = new (); private readonly BufferWriter metadataBuffer = new (128); private readonly BufferWriter indexBuffer = new (24); @@ -360,4 +361,4 @@ private void UpdatePageIndex(int bytes, Envelope lastEnvelope) } } } -} +} \ No newline at end of file diff --git a/Sources/Runtime/Microsoft.Psi/Remoting/RemoteClockImporter.cs b/Sources/Runtime/Microsoft.Psi/Remoting/RemoteClockImporter.cs index 31ba18a7c..293cf067a 100644 --- a/Sources/Runtime/Microsoft.Psi/Remoting/RemoteClockImporter.cs +++ b/Sources/Runtime/Microsoft.Psi/Remoting/RemoteClockImporter.cs @@ -72,10 +72,10 @@ private void SynchronizeLocalPipelineClock() NetworkStream networkStream = null; try { - Trace.WriteLine($"Attempting to connect to {this.port}"); + Trace.WriteLine($"Attempting to connect to {this.host} on port {this.port} ..."); this.client.Connect(this.host, this.port); networkStream = this.client.GetStream(); - Trace.WriteLine($"Connected to {this.port}."); + Trace.WriteLine($"Connected to {this.host} on port {this.port}."); // send protocol version using var writer = new BinaryWriter(networkStream); diff --git a/Sources/Runtime/Microsoft.Psi/Remoting/RemoteExporter.cs b/Sources/Runtime/Microsoft.Psi/Remoting/RemoteExporter.cs index 0935318d6..12b1772f1 100644 --- a/Sources/Runtime/Microsoft.Psi/Remoting/RemoteExporter.cs +++ b/Sources/Runtime/Microsoft.Psi/Remoting/RemoteExporter.cs @@ -401,7 +401,7 @@ private void Disconnect() this.Dispose(); } - private void MetaUpdateHandler(IEnumerable meta, RuntimeInfo runtimeVersion) + private void MetaUpdateHandler(IEnumerable meta, RuntimeInfo runtimeInfo) { try { diff --git a/Sources/Runtime/Microsoft.Psi/Scheduling/FutureWorkItemQueue.cs b/Sources/Runtime/Microsoft.Psi/Scheduling/FutureWorkItemQueue.cs index 501f71349..5ce421e3a 100644 --- a/Sources/Runtime/Microsoft.Psi/Scheduling/FutureWorkItemQueue.cs +++ b/Sources/Runtime/Microsoft.Psi/Scheduling/FutureWorkItemQueue.cs @@ -8,7 +8,7 @@ namespace Microsoft.Psi.Scheduling /// internal class FutureWorkItemQueue : PriorityQueue { - private Scheduler scheduler; + private readonly Scheduler scheduler; public FutureWorkItemQueue(string name, Scheduler scheduler) : base(name, WorkItem.PriorityCompare) @@ -18,10 +18,14 @@ public FutureWorkItemQueue(string name, Scheduler scheduler) protected override bool DequeueCondition(WorkItem item) { - // Dequeue work item if it is due for execution, or will never be executed - // due to the scheduler context being finalized before its execution time. - return item.StartTime <= this.scheduler.Clock.GetCurrentTime() - || item.StartTime > item.SchedulerContext.FinalizeTime; + // Dequeue work item if: + // (1) it is due for execution, or + // (2) the scheduler does not need to delay future work items, or + // (3) it will never be executed due to the scheduler context being + // finalized before its execution time. + return item.StartTime <= this.scheduler.Clock.GetCurrentTime() || + !this.scheduler.DelayFutureWorkItemsUntilDue || + item.StartTime > item.SchedulerContext.FinalizeTime; } } } diff --git a/Sources/Runtime/Microsoft.Psi/Scheduling/Scheduler.cs b/Sources/Runtime/Microsoft.Psi/Scheduling/Scheduler.cs index c601ea031..de7076920 100644 --- a/Sources/Runtime/Microsoft.Psi/Scheduling/Scheduler.cs +++ b/Sources/Runtime/Microsoft.Psi/Scheduling/Scheduler.cs @@ -15,11 +15,11 @@ public sealed class Scheduler : IDisposable private readonly SimpleSemaphore threadSemaphore; private readonly Func errorHandler; private readonly bool allowSchedulingOnExternalThreads; - private readonly ManualResetEvent stopped = new ManualResetEvent(true); - private readonly AutoResetEvent futureQueuePulse = new AutoResetEvent(false); - private readonly ThreadLocal nextWorkitem = new ThreadLocal(); - private readonly ThreadLocal isSchedulerThread = new ThreadLocal(() => false); - private readonly ThreadLocal currentWorkitemTime = new ThreadLocal(() => DateTime.MaxValue); + private readonly ManualResetEvent stopped = new (true); + private readonly AutoResetEvent futureQueuePulse = new (false); + private readonly ThreadLocal nextWorkitem = new (); + private readonly ThreadLocal isSchedulerThread = new (() => false); + private readonly ThreadLocal currentWorkitemTime = new (() => DateTime.MaxValue); // the queue of pending workitems, ordered by start time private readonly WorkItemQueue globalWorkitems; @@ -28,7 +28,7 @@ public sealed class Scheduler : IDisposable private IPerfCounterCollection counters; private bool forcedShutdownRequested; private Clock clock; - private bool delayFutureWorkitemsUntilDue; + private bool delayFutureWorkItemsUntilDue; private bool started = false; private bool completed = false; @@ -52,7 +52,7 @@ public Scheduler(Func errorHandler, int threadCount = 0, bool a // set virtual time such that any scheduled item appears to be in the future and gets queued in the future workitem queue // the time will change when the scheduler is started, and the future workitem queue will be drained then as appropriate this.clock = clock ?? new Clock(DateTime.MinValue, 0); - this.delayFutureWorkitemsUntilDue = true; + this.delayFutureWorkItemsUntilDue = true; } /// @@ -69,6 +69,11 @@ internal Scheduler(Scheduler parent) /// public Clock Clock => this.clock; + /// + /// Gets a value indicating whether the scheduler should delay future work items until they're due. + /// + internal bool DelayFutureWorkItemsUntilDue => this.delayFutureWorkItemsUntilDue; + internal bool IsStarted { get @@ -122,7 +127,7 @@ public void Dispose() return true; } - if (startTime > this.clock.GetCurrentTime() && this.delayFutureWorkitemsUntilDue) + if (startTime > this.clock.GetCurrentTime() && this.delayFutureWorkItemsUntilDue) { return false; } @@ -189,7 +194,7 @@ public bool TryExecute(SynchronizationLock synchronizationObject, Action action, return true; } - if (startTime > this.clock.GetCurrentTime() && this.delayFutureWorkitemsUntilDue) + if (startTime > this.clock.GetCurrentTime() && this.delayFutureWorkItemsUntilDue) { return false; } @@ -274,7 +279,7 @@ public void Start(Clock clock, bool delayFutureWorkitemsUntilDue) } // if no clock is specified, schedule everything without delay - this.delayFutureWorkitemsUntilDue = delayFutureWorkitemsUntilDue; + this.delayFutureWorkItemsUntilDue = delayFutureWorkitemsUntilDue; this.clock = clock; this.started = true; this.stopped.Reset(); @@ -298,7 +303,7 @@ public void Stop(bool abandonPendingWorkitems = false) this.futuresThread.Join(); this.clock = new Clock(DateTime.MinValue, 0); this.completed = true; - this.delayFutureWorkitemsUntilDue = true; + this.delayFutureWorkItemsUntilDue = true; } /// @@ -431,7 +436,7 @@ private void Schedule(WorkItem wi, bool asContinuation = true) } // if the work item is not yet due, add it to the future work item queue, as long as it is not after the finalize time - if ((wi.StartTime > wi.SchedulerContext.Clock.GetCurrentTime() && wi.StartTime <= wi.SchedulerContext.FinalizeTime && this.delayFutureWorkitemsUntilDue) || + if ((wi.StartTime > wi.SchedulerContext.Clock.GetCurrentTime() && wi.StartTime <= wi.SchedulerContext.FinalizeTime && this.delayFutureWorkItemsUntilDue) || !this.started || !wi.SchedulerContext.Started) { this.futureWorkitems.Enqueue(wi); @@ -630,7 +635,7 @@ private void ProcessFutureQueue() if (this.futureWorkitems.TryPeek(out wi)) { // the result could be a negative value if some other thread captured "now" before us and added the item to the future queue after the while loop above exited - waitTimeout = (int)this.clock.ToRealTime(wi.StartTime - this.clock.GetCurrentTime()).TotalMilliseconds; + waitTimeout = this.delayFutureWorkItemsUntilDue ? (int)this.clock.ToRealTime(wi.StartTime - this.clock.GetCurrentTime()).TotalMilliseconds : 0; if (waitTimeout < 0) { waitTimeout = 0; diff --git a/Sources/Runtime/Microsoft.Psi/Serialization/ArraySerializer.cs b/Sources/Runtime/Microsoft.Psi/Serialization/ArraySerializer.cs index d1ae80760..3ab147da4 100644 --- a/Sources/Runtime/Microsoft.Psi/Serialization/ArraySerializer.cs +++ b/Sources/Runtime/Microsoft.Psi/Serialization/ArraySerializer.cs @@ -12,7 +12,7 @@ namespace Microsoft.Psi.Serialization /// The type of objects this serializer knows how to handle. internal sealed class ArraySerializer : ISerializer { - private const int Version = 2; + private const int LatestSchemaVersion = 2; private SerializationHandler elementHandler; @@ -23,9 +23,17 @@ public TypeSchema Initialize(KnownSerializers serializers, TypeSchema targetSche { var type = typeof(T[]); this.elementHandler = serializers.GetHandler(); // register element type - var name = TypeSchema.GetContractName(type, serializers.RuntimeVersion); + var name = TypeSchema.GetContractName(type, serializers.RuntimeInfo.SerializationSystemVersion); var elementsMember = new TypeMemberSchema("Elements", typeof(T).AssemblyQualifiedName, true); - var schema = new TypeSchema(name, TypeSchema.GetId(name), type.AssemblyQualifiedName, TypeFlags.IsCollection, new TypeMemberSchema[] { elementsMember }, Version); + var schema = new TypeSchema( + type.AssemblyQualifiedName, + TypeFlags.IsCollection, + new TypeMemberSchema[] { elementsMember }, + name, + TypeSchema.GetId(name), + LatestSchemaVersion, + this.GetType().AssemblyQualifiedName, + serializers.RuntimeInfo.SerializationSystemVersion); return targetSchema ?? schema; } @@ -78,7 +86,7 @@ private void PrepareTarget(ref T[] target, int size, SerializationContext contex if (target != null && target.Length > size && (!this.elementHandler.IsClearRequired.HasValue || this.elementHandler.IsClearRequired.Value)) { // use a separate context to clear the unused objects, so that we don't corrupt the current context - SerializationContext clearContext = new SerializationContext(context.Serializers); + var clearContext = new SerializationContext(context.Serializers); // only clear the extra items that we won't use during cloning or deserialization (those get cleared by cloning/deserialization). for (int i = size; i < target.Length; i++) diff --git a/Sources/Runtime/Microsoft.Psi/Serialization/BackCompatClassSerializer.cs b/Sources/Runtime/Microsoft.Psi/Serialization/BackCompatClassSerializer.cs new file mode 100644 index 000000000..c2f7f8b32 --- /dev/null +++ b/Sources/Runtime/Microsoft.Psi/Serialization/BackCompatClassSerializer.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.Psi.Serialization +{ + /// + /// Provides a base class for authoring backwards compatible custom serializers (for reading) for class types. + /// + /// The type of objects handled by the custom serializer. + public abstract class BackCompatClassSerializer : BackCompatSerializer + where T : class + { + /// + /// Initializes a new instance of the class. + /// + /// The current schema version. + public BackCompatClassSerializer(int schemaVersion) + : base(schemaVersion, new ClassSerializer()) + { + } + } +} diff --git a/Sources/Runtime/Microsoft.Psi/Serialization/BackCompatSerializer.cs b/Sources/Runtime/Microsoft.Psi/Serialization/BackCompatSerializer.cs new file mode 100644 index 000000000..927f0869e --- /dev/null +++ b/Sources/Runtime/Microsoft.Psi/Serialization/BackCompatSerializer.cs @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.Psi.Serialization +{ + using System; + using Microsoft.Psi.Common; + + /// + /// Provides a base class for authoring backwards compatible custom serializers (for reading). + /// + /// The type of objects handled by the custom serializer. + public abstract class BackCompatSerializer : ISerializer + { + private readonly int latestSchemaVersion; + private readonly ISerializer latestVersionSerializer; + private int targetSchemaVersion; + + /// + /// Initializes a new instance of the class. + /// + /// The latest (current) schema version. + /// The latest version (current) serializer. + public BackCompatSerializer( + int latestSchemaVersion, + ISerializer latestVersionSerializer) + { + this.latestSchemaVersion = latestSchemaVersion; + this.latestVersionSerializer = latestVersionSerializer; + } + + /// + public bool? IsClearRequired => this.latestVersionSerializer.IsClearRequired; + + /// + public TypeSchema Initialize(KnownSerializers serializers, TypeSchema targetSchema) + { + // Capture the target schema version + this.targetSchemaVersion = targetSchema != null ? targetSchema.Version : this.latestSchemaVersion; + + // Create the latest version schema + var latestVersionSchema = TypeSchema.FromType( + typeof(T), + this.latestVersionSerializer.GetType().AssemblyQualifiedName, + this.latestSchemaVersion, + serializers.RuntimeInfo.SerializationSystemVersion); + + // Initialize back-compat handlers + if (this.targetSchemaVersion != this.latestSchemaVersion) + { + this.InitializeBackCompatSerializationHandlers(this.targetSchemaVersion, serializers, targetSchema); + } + + // Initialize the latest version serializer + this.latestVersionSerializer.Initialize(serializers, latestVersionSchema); + + return targetSchema ?? latestVersionSchema; + } + + /// + /// Abstract base method for initializing back-compat serialization handlers. + /// + /// The schema version to deserialize. + /// The set of serialization handlers. + /// When the serializer is used to deserialize existing data, this parameter provides the schema that was persisted with the data. + public abstract void InitializeBackCompatSerializationHandlers(int schemaVersion, KnownSerializers serializers, TypeSchema targetSchema); + + /// + public void Serialize(BufferWriter writer, T instance, SerializationContext context) + { + if (this.targetSchemaVersion != this.latestSchemaVersion) + { + throw new NotSupportedException($"The back compat serializer does not support {nameof(this.Serialize)} when using a previous schema version."); + } + + this.latestVersionSerializer.Serialize(writer, instance, context); + } + + /// + public void PrepareDeserializationTarget(BufferReader reader, ref T target, SerializationContext context) + { + this.latestVersionSerializer.PrepareDeserializationTarget(reader, ref target, context); + } + + /// + public void Deserialize(BufferReader reader, ref T target, SerializationContext context) + { + if (this.targetSchemaVersion == this.latestSchemaVersion) + { + this.latestVersionSerializer.Deserialize(reader, ref target, context); + } + else + { + this.BackCompatDeserialize(this.targetSchemaVersion, reader, ref target, context); + } + } + + /// + /// Abstract method for back-compatible deserialization. + /// + /// The schema version to deserialize. + /// The stream reader to deserialize from. + /// An instance to deserialize into. + /// A context object containing accumulated type mappings and object references. + public abstract void BackCompatDeserialize(int schemaVersion, BufferReader reader, ref T target, SerializationContext context); + + /// + public void PrepareCloningTarget(T instance, ref T target, SerializationContext context) + => this.latestVersionSerializer.PrepareCloningTarget(instance, ref target, context); + + /// + public void Clone(T instance, ref T target, SerializationContext context) + => this.latestVersionSerializer.Clone(instance, ref target, context); + + /// + public void Clear(ref T target, SerializationContext context) + => this.latestVersionSerializer.Clear(ref target, context); + } +} diff --git a/Sources/Runtime/Microsoft.Psi/Serialization/BackCompatStructSerializer.cs b/Sources/Runtime/Microsoft.Psi/Serialization/BackCompatStructSerializer.cs new file mode 100644 index 000000000..b10d34310 --- /dev/null +++ b/Sources/Runtime/Microsoft.Psi/Serialization/BackCompatStructSerializer.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.Psi.Serialization +{ + /// + /// Provides a base class for authoring backwards compatible custom serializers (for reading) for struct types. + /// + /// The type of objects handled by the custom serializer. + public abstract class BackCompatStructSerializer : BackCompatSerializer + where T : struct + { + /// + /// Initializes a new instance of the class. + /// + /// The current schema version. + public BackCompatStructSerializer(int schemaVersion) + : base(schemaVersion, new StructSerializer()) + { + } + } +} diff --git a/Sources/Runtime/Microsoft.Psi/Serialization/BufferSerializer.cs b/Sources/Runtime/Microsoft.Psi/Serialization/BufferSerializer.cs index 09483c0b9..db69e243a 100644 --- a/Sources/Runtime/Microsoft.Psi/Serialization/BufferSerializer.cs +++ b/Sources/Runtime/Microsoft.Psi/Serialization/BufferSerializer.cs @@ -12,7 +12,7 @@ namespace Microsoft.Psi.Serialization /// internal sealed class BufferSerializer : ISerializer { - private const int Version = 2; + private const int LatestSchemaVersion = 2; /// public bool? IsClearRequired => false; @@ -21,9 +21,17 @@ public TypeSchema Initialize(KnownSerializers serializers, TypeSchema targetSche { serializers.GetHandler(); // register element type var type = typeof(byte[]); - var name = TypeSchema.GetContractName(type, serializers.RuntimeVersion); + var name = TypeSchema.GetContractName(type, serializers.RuntimeInfo.SerializationSystemVersion); var elementsMember = new TypeMemberSchema("Elements", typeof(byte).AssemblyQualifiedName, true); - var schema = new TypeSchema(name, TypeSchema.GetId(name), type.AssemblyQualifiedName, TypeFlags.IsCollection, new TypeMemberSchema[] { elementsMember }, Version); + var schema = new TypeSchema( + type.AssemblyQualifiedName, + TypeFlags.IsCollection, + new TypeMemberSchema[] { elementsMember }, + name, + TypeSchema.GetId(name), + LatestSchemaVersion, + this.GetType().AssemblyQualifiedName, + serializers.RuntimeInfo.SerializationSystemVersion); return targetSchema ?? schema; } @@ -42,7 +50,7 @@ public void Serialize(BufferWriter writer, BufferReader instance, SerializationC public void PrepareDeserializationTarget(BufferReader reader, ref BufferReader target, SerializationContext context) { var length = reader.ReadInt32(); - target = target ?? new BufferReader(); + target ??= new BufferReader(); target.Reset(length); } diff --git a/Sources/Runtime/Microsoft.Psi/Serialization/ByteArraySerializer.cs b/Sources/Runtime/Microsoft.Psi/Serialization/ByteArraySerializer.cs index 7a5922a3a..d466f79d0 100644 --- a/Sources/Runtime/Microsoft.Psi/Serialization/ByteArraySerializer.cs +++ b/Sources/Runtime/Microsoft.Psi/Serialization/ByteArraySerializer.cs @@ -15,7 +15,7 @@ namespace Microsoft.Psi.Serialization /// internal sealed class ByteArraySerializer : ISerializer { - private const int Version = 2; + private const int LatestSchemaVersion = 2; /// public bool? IsClearRequired => false; @@ -24,9 +24,17 @@ public TypeSchema Initialize(KnownSerializers serializers, TypeSchema targetSche { serializers.GetHandler(); // register element type var type = typeof(byte[]); - var name = TypeSchema.GetContractName(type, serializers.RuntimeVersion); + var name = TypeSchema.GetContractName(type, serializers.RuntimeInfo.SerializationSystemVersion); var elementsMember = new TypeMemberSchema("Elements", typeof(byte).AssemblyQualifiedName, true); - var schema = new TypeSchema(name, TypeSchema.GetId(name), type.AssemblyQualifiedName, TypeFlags.IsCollection, new TypeMemberSchema[] { elementsMember }, Version); + var schema = new TypeSchema( + type.AssemblyQualifiedName, + TypeFlags.IsCollection, + new TypeMemberSchema[] { elementsMember }, + name, + TypeSchema.GetId(name), + LatestSchemaVersion, + this.GetType().AssemblyQualifiedName, + serializers.RuntimeInfo.SerializationSystemVersion); return targetSchema ?? schema; } diff --git a/Sources/Runtime/Microsoft.Psi/Serialization/ClassSerializer.cs b/Sources/Runtime/Microsoft.Psi/Serialization/ClassSerializer.cs index 11583a31b..0ca585199 100644 --- a/Sources/Runtime/Microsoft.Psi/Serialization/ClassSerializer.cs +++ b/Sources/Runtime/Microsoft.Psi/Serialization/ClassSerializer.cs @@ -15,8 +15,6 @@ namespace Microsoft.Psi.Serialization /// The type of objects this serializer knows how to handle. internal class ClassSerializer : ISerializer { - private const int Version = 1; - // we use delegates (instead of generating a full class) because dynamic delegates (unlike dynamic types) // can access the private fields of the target type. private SerializeDelegate serializeImpl; @@ -38,7 +36,7 @@ public ClassSerializer() public TypeSchema Initialize(KnownSerializers serializers, TypeSchema targetSchema) { - var runtimeSchema = TypeSchema.FromType(typeof(T), serializers.RuntimeVersion, this.GetType(), Version); + var runtimeSchema = TypeSchema.FromType(typeof(T), this.GetType().AssemblyQualifiedName, serializers.RuntimeInfo.SerializationSystemVersion); var members = runtimeSchema.GetCompatibleMemberSet(targetSchema); this.deserializeImpl = Generator.GenerateDeserializeMethod(il => Generator.EmitDeserializeFields(typeof(T), serializers, il, members)); @@ -130,10 +128,7 @@ public void Clear(ref T target, SerializationContext context) [MethodImpl(MethodImplOptions.AggressiveInlining)] public void PrepareCloningTarget(T instance, ref T target, SerializationContext context) { - if (target == null) - { - target = (T)FormatterServices.GetUninitializedObject(typeof(T)); - } + target ??= (T)FormatterServices.GetUninitializedObject(typeof(T)); } /// @@ -145,10 +140,7 @@ public void PrepareCloningTarget(T instance, ref T target, SerializationContext [MethodImpl(MethodImplOptions.AggressiveInlining)] public void PrepareDeserializationTarget(BufferReader reader, ref T target, SerializationContext context) { - if (target == null) - { - target = (T)FormatterServices.GetUninitializedObject(typeof(T)); - } + target ??= (T)FormatterServices.GetUninitializedObject(typeof(T)); } } } diff --git a/Sources/Runtime/Microsoft.Psi/Serialization/DictionarySerializer.cs b/Sources/Runtime/Microsoft.Psi/Serialization/DictionarySerializer.cs index 42d2c3b89..1dcce86c6 100644 --- a/Sources/Runtime/Microsoft.Psi/Serialization/DictionarySerializer.cs +++ b/Sources/Runtime/Microsoft.Psi/Serialization/DictionarySerializer.cs @@ -14,7 +14,7 @@ namespace Microsoft.Psi.Serialization internal sealed class DictionarySerializer : ISerializer> { // Bumping the schema version number from the auto-generated version which defaults to RuntimeInfo.CurrentRuntimeVersion (2) - private const int SchemaVersion = 3; + private const int LatestSchemaVersion = 3; private ISerializer> innerSerializer; @@ -103,12 +103,20 @@ public TypeSchema Initialize(KnownSerializers serializers, TypeSchema targetSche this.entriesHandler = serializers.GetHandler[]>(); var type = typeof(Dictionary); - var name = TypeSchema.GetContractName(type, serializers.RuntimeVersion); + var name = TypeSchema.GetContractName(type, serializers.RuntimeInfo.SerializationSystemVersion); // Treat the Dictionary as a class with 2 members - a comparer and an array of key-value pairs var comparerMember = new TypeMemberSchema("Comparer", typeof(IEqualityComparer).AssemblyQualifiedName, true); var entriesMember = new TypeMemberSchema("KeyValuePairs", typeof(KeyValuePair[]).AssemblyQualifiedName, true); - var schema = new TypeSchema(name, TypeSchema.GetId(name), type.AssemblyQualifiedName, TypeFlags.IsClass, new[] { comparerMember, entriesMember }, SchemaVersion); + var schema = new TypeSchema( + type.AssemblyQualifiedName, + TypeFlags.IsClass, + new[] { comparerMember, entriesMember }, + name, + TypeSchema.GetId(name), + LatestSchemaVersion, + this.GetType().AssemblyQualifiedName, + serializers.RuntimeInfo.SerializationSystemVersion); return targetSchema ?? schema; } diff --git a/Sources/Runtime/Microsoft.Psi/Serialization/DynamicMessageDeserializer.cs b/Sources/Runtime/Microsoft.Psi/Serialization/DynamicMessageDeserializer.cs index 34070728d..a954a032c 100644 --- a/Sources/Runtime/Microsoft.Psi/Serialization/DynamicMessageDeserializer.cs +++ b/Sources/Runtime/Microsoft.Psi/Serialization/DynamicMessageDeserializer.cs @@ -7,6 +7,7 @@ namespace Microsoft.Psi.Serialization using System.Collections.Generic; using System.Dynamic; using System.Linq; + using System.Runtime.Serialization; using Microsoft.Psi.Common; /// @@ -18,18 +19,22 @@ internal sealed class DynamicMessageDeserializer private readonly string rootTypeName; private readonly IDictionary schemasByTypeName; private readonly IDictionary schemasById; - private readonly List instanceCache = new List(); + private readonly List instanceCache = new (); + private readonly IDictionary typeNameSynonyms; + private readonly XsdDataContractExporter dataContractExporter = new (); /// /// Initializes a new instance of the class. /// /// Type name of message. /// Collection of known TypeSchemas. - public DynamicMessageDeserializer(string typeName, IDictionary schemas) + /// Type name synonyms. + public DynamicMessageDeserializer(string typeName, IDictionary schemas, IDictionary typeNameSynonyms) { this.rootTypeName = typeName; this.schemasByTypeName = schemas; this.schemasById = schemas.Values.ToDictionary(s => s.Id); + this.typeNameSynonyms = typeNameSynonyms; } /// @@ -80,12 +85,42 @@ private dynamic Read(string typeName, BufferReader reader, bool isCollectionElem // determine type info and schema var isString = simpleTypeName == "System.String"; - if (!isString && !this.schemasByTypeName.ContainsKey(typeName)) { - // try custom serializer - var prefix = typeName.Split('[', ',')[0]; - typeName = $"{prefix}+CustomSerializer{typeName.Substring(prefix.Length)}"; + string ResolveTypeName() + { + if (this.typeNameSynonyms.TryGetValue(typeName, out var synonym)) + { + return synonym; + } + else + { + // try contract name (if type can be resolved) + var typ = Type.GetType(typeName, false); + if (typ != null) + { + var contractName = this.dataContractExporter.GetSchemaTypeName(typ); + if (contractName != null) + { + synonym = contractName.ToString(); + this.typeNameSynonyms.Add(typeName, synonym); + return synonym; + } + } + + // try custom serializer + var prefix = typeName.Split('[', ',')[0]; + var customTypeName = $"{prefix}+CustomSerializer{typeName.Substring(prefix.Length)}"; + if (!this.schemasByTypeName.ContainsKey(customTypeName)) + { + throw new Exception($"Unknown schema type name ({typeName}).\nA synonym may be needed (see {nameof(KnownSerializers)}.{nameof(KnownSerializers.RegisterDynamicTypeSchemaNameSynonym)}())"); + } + + return customTypeName; + } + } + + typeName = ResolveTypeName(); } var schema = isString ? null : this.schemasByTypeName[typeName]; diff --git a/Sources/Runtime/Microsoft.Psi/Serialization/EnumerableSerializer.cs b/Sources/Runtime/Microsoft.Psi/Serialization/EnumerableSerializer.cs index 405cdbf26..6a935b39d 100644 --- a/Sources/Runtime/Microsoft.Psi/Serialization/EnumerableSerializer.cs +++ b/Sources/Runtime/Microsoft.Psi/Serialization/EnumerableSerializer.cs @@ -14,7 +14,7 @@ namespace Microsoft.Psi.Serialization /// The type of objects this serializer knows how to handle. internal sealed class EnumerableSerializer : ISerializer> { - private const int Version = 2; + private const int LatestSchemaVersion = 2; private SerializationHandler elementHandler; @@ -25,9 +25,17 @@ public TypeSchema Initialize(KnownSerializers serializers, TypeSchema targetSche { this.elementHandler = serializers.GetHandler(); // register element type var type = typeof(T[]); - var name = TypeSchema.GetContractName(type, serializers.RuntimeVersion); + var name = TypeSchema.GetContractName(type, serializers.RuntimeInfo.SerializationSystemVersion); var elementsMember = new TypeMemberSchema("Elements", typeof(T).AssemblyQualifiedName, true); - var schema = new TypeSchema(name, TypeSchema.GetId(name), type.AssemblyQualifiedName, TypeFlags.IsCollection, new TypeMemberSchema[] { elementsMember }, Version); + var schema = new TypeSchema( + type.AssemblyQualifiedName, + TypeFlags.IsCollection, + new TypeMemberSchema[] { elementsMember }, + name, + TypeSchema.GetId(name), + LatestSchemaVersion, + this.GetType().AssemblyQualifiedName, + serializers.RuntimeInfo.SerializationSystemVersion); return targetSchema ?? schema; } diff --git a/Sources/Runtime/Microsoft.Psi/Serialization/ISerializer.cs b/Sources/Runtime/Microsoft.Psi/Serialization/ISerializer.cs index 210ad7cf6..1d10104ad 100644 --- a/Sources/Runtime/Microsoft.Psi/Serialization/ISerializer.cs +++ b/Sources/Runtime/Microsoft.Psi/Serialization/ISerializer.cs @@ -42,7 +42,7 @@ public interface ISerializer /// its parent. /// Note that targetSchema is a partial schema, without any MemberInfo information. /// To obtain MemberInfo information, generate a schema from the runtime type - /// using . + /// using . /// TypeSchema Initialize(KnownSerializers serializers, TypeSchema targetSchema); diff --git a/Sources/Runtime/Microsoft.Psi/Serialization/ImmutableSerializer.cs b/Sources/Runtime/Microsoft.Psi/Serialization/ImmutableSerializer.cs index e9c1f796a..10cdeb2a0 100644 --- a/Sources/Runtime/Microsoft.Psi/Serialization/ImmutableSerializer.cs +++ b/Sources/Runtime/Microsoft.Psi/Serialization/ImmutableSerializer.cs @@ -12,7 +12,6 @@ namespace Microsoft.Psi.Serialization /// The type of objects this serializer knows how to handle. internal class ImmutableSerializer : ISerializer { - private const int Version = 1; private SerializeDelegate serializeImpl; private DeserializeDelegate deserializeImpl; @@ -25,7 +24,7 @@ public ImmutableSerializer() public TypeSchema Initialize(KnownSerializers serializers, TypeSchema targetSchema) { - var runtimeSchema = TypeSchema.FromType(typeof(T), serializers.RuntimeVersion, this.GetType(), Version); + var runtimeSchema = TypeSchema.FromType(typeof(T), this.GetType().AssemblyQualifiedName, serializers.RuntimeInfo.SerializationSystemVersion); var members = runtimeSchema.GetCompatibleMemberSet(targetSchema); this.serializeImpl = Generator.GenerateSerializeMethod(il => Generator.EmitSerializeFields(typeof(T), serializers, il, members)); diff --git a/Sources/Runtime/Microsoft.Psi/Serialization/KnownSerializers.cs b/Sources/Runtime/Microsoft.Psi/Serialization/KnownSerializers.cs index c218930a5..5252c78a0 100644 --- a/Sources/Runtime/Microsoft.Psi/Serialization/KnownSerializers.cs +++ b/Sources/Runtime/Microsoft.Psi/Serialization/KnownSerializers.cs @@ -72,38 +72,41 @@ public class KnownSerializers public static readonly KnownSerializers Default; // the set of types we don't know how to serialize - private static readonly HashSet UnserializableTypes = new HashSet(); + private static readonly HashSet UnserializableTypes = new (); + + // mapping from fully-qualified .NET type names to synonyms + private readonly Dictionary typeNameSynonyms = new (); // the serialization subsystem version used by this instance - private RuntimeInfo runtimeVersion; + private readonly RuntimeInfo runtimeInfo; // the default instance marker - private bool isDefault; + private readonly bool isDefault; - private object syncRoot = new object(); + private readonly object syncRoot = new (); // *************** the rules for creating serializers **************** // the custom generic serializers, such as SharedSerializer>, that need to be instantiated for every T - private Dictionary templates; + private readonly Dictionary templates; // the custom serializers, such as ManagedBufferSerializer, which have been registered explicitly rather than with class annotations - private Dictionary serializers; + private readonly Dictionary serializers; // used to find a type for a given schema (when creating a handler from a polymorphic field: id -> schema -> type) - private ConcurrentDictionary knownTypes; + private readonly ConcurrentDictionary knownTypes; // used to find the name from a type (when creating a handler: type -> string -> schema) - private ConcurrentDictionary knownNames; + private readonly ConcurrentDictionary knownNames; // used to find schema for a given contract name (when creating a handler: type -> string -> schema) - private ConcurrentDictionary schemas; + private readonly ConcurrentDictionary schemas; // used to find the schema by id (when creating a handler from a polymorphic field: id -> schema -> type) - private ConcurrentDictionary schemasById; + private readonly ConcurrentDictionary schemasById; // used to find the cloning flags for a given type - private ConcurrentDictionary cloningFlags; + private readonly ConcurrentDictionary cloningFlags; // *************** the cached handlers and handler indexes **************** // these caches are accessed often once the rules are expanded into handlers, @@ -128,24 +131,24 @@ static KnownSerializers() UnserializableTypes.Add(typeof(UIntPtr)); UnserializableTypes.Add(typeof(MemberInfo)); UnserializableTypes.Add(typeof(System.Diagnostics.StackTrace)); - Default = new KnownSerializers(true, RuntimeInfo.Current); + Default = new KnownSerializers(true, RuntimeInfo.Latest); } /// /// Initializes a new instance of the class. /// - /// + /// /// The version of the runtime to be compatible with. This dictates the behavior of automatic serialization. /// - public KnownSerializers(RuntimeInfo runtimeVersion = null) - : this(false, runtimeVersion ?? RuntimeInfo.Current) + public KnownSerializers(RuntimeInfo runtimeInfo = null) + : this(false, runtimeInfo ?? RuntimeInfo.Latest) { } - private KnownSerializers(bool isDefault, RuntimeInfo runtimeVersion) + private KnownSerializers(bool isDefault, RuntimeInfo runtimeInfo) { this.isDefault = isDefault; - this.runtimeVersion = runtimeVersion; // this can change because of metadata updates! + this.runtimeInfo = runtimeInfo; // this can change because of metadata updates! this.handlers = new SerializationHandler[0]; this.handlersById = new Dictionary(); @@ -190,15 +193,20 @@ private KnownSerializers(bool isDefault, RuntimeInfo runtimeVersion) public event EventHandler SchemaAdded; /// - /// Gets the version of the serialization subsystem this serializer set is compatible with. + /// Gets the runtime info, including the version of the serialization subsystem. /// - public RuntimeInfo RuntimeVersion => this.runtimeVersion; + public RuntimeInfo RuntimeInfo => this.runtimeInfo; /// /// Gets the set of schemas in use. /// public IDictionary Schemas => this.schemas; + /// + /// Gets the set of type name synonyms. + /// + public IDictionary TypeNameSynonyms => this.typeNameSynonyms; + /// /// Registers type T with the specified contract name. /// Use this overload to deserialize data persisted before a type name change. @@ -217,7 +225,7 @@ private KnownSerializers(bool isDefault, RuntimeInfo runtimeVersion) /// Optional flags that control the cloning behavior for this type. public void Register(Type type, string contractName, CloningFlags cloningFlags = CloningFlags.None) { - contractName ??= TypeSchema.GetContractName(type, this.runtimeVersion); + contractName ??= TypeSchema.GetContractName(type, this.runtimeInfo.SerializationSystemVersion); if (this.knownTypes.TryGetValue(contractName, out Type existingType) && existingType != type) { throw new SerializationException($"Cannot register type {type.AssemblyQualifiedName} under the contract name {contractName} because the type {existingType.AssemblyQualifiedName} is already registered under the same name."); @@ -287,13 +295,50 @@ public void Register(Type type, string contractName, CloningFlags cloningFlags = /// The type of generic serializer to register. public void RegisterGenericSerializer(Type genericSerializer) { - // var interf = genericSerializer.GetInterface("ISerializer`1"); var interf = genericSerializer.GetInterface(typeof(ISerializer<>).FullName); var serializableType = interf.GetGenericArguments()[0]; serializableType = TypeResolutionHelper.GetVerifiedType(serializableType.Namespace + "." + serializableType.Name); // FullName doesn't work here this.templates[serializableType] = genericSerializer; } + /// + /// Register synonym for fully-qualified type name. + /// + /// Synonym used in type-schema info. + /// Fully-qualified .NET type name. + public void RegisterDynamicTypeSchemaNameSynonym(string synonym, string fullyQualifiedTypeName) + { + this.typeNameSynonyms.Add(fullyQualifiedTypeName, synonym); + } + + /// + /// Gets the serialization handler for a specified type. + /// + /// The type to get the serialization handler for. + /// The serialization handler for a specified type. + /// + /// This is the slow-ish path, called at codegen time, from custom serializers that want to cache a handler + /// and for polymorphic fields, the first time the id is encountered. + /// + public SerializationHandler GetHandler() + { + // important: all code paths that could lead to the creation of a new handler need to lock. + // We want to make sure a handler is fully created and initialized before it is returned, so we lock before accessing the dictionary + // A thread that is generating code can re-enter here as it is expanding the type graph, + // and can get a partially initialized handler to resolve circular type references + // but other threads have to wait for the expansion to finish. + lock (this.syncRoot) + { + // if we don't have one already, create one + if (!this.handlersByType.TryGetValue(typeof(T), out SerializationHandler handler)) + { + handler = this.AddHandler(); + } + + return (SerializationHandler)handler; + } + } + /// /// Gets the cloning flags for the specified type. /// @@ -342,7 +387,7 @@ internal void RegisterMetadata(IEnumerable metadata) // v0 has runtime types affixed to each stream metadata foreach (var kv in sm.RuntimeTypes) { - var schema = new TypeSchema(kv.Value, kv.Key, kv.Value, 0); + var schema = new TypeSchema(kv.Value, kv.Value, kv.Key, 0, null, 0); this.RegisterSchema(schema); } } @@ -354,27 +399,6 @@ internal void RegisterMetadata(IEnumerable metadata) } } - // this is the slow-ish path, called at codegen time, from custom serializers that want to cache a handler - // and for polymorphic fields, the first time the id is encountered - internal SerializationHandler GetHandler() - { - // important: all code paths that could lead to the creation of a new handler need to lock. - // We want to make sure a handler is fully created and initialized before it is returned, so we lock before accessing the dictionary - // A thread that is generating code can re-enter here as it is expanding the type graph, - // and can get a partially initialized handler to resolve circular type references - // but other threads have to wait for the expansion to finish. - lock (this.syncRoot) - { - // if we don't have one already, create one - if (!this.handlersByType.TryGetValue(typeof(T), out SerializationHandler handler)) - { - handler = this.AddHandler(); - } - - return (SerializationHandler)handler; - } - } - // called during codegen, to turn the index of the handler into a constant in the emitted code internal int GetIndex(SerializationHandler handler) => this.index[handler]; @@ -393,7 +417,7 @@ internal SerializationHandler GetUntypedHandler(Type type) } } - var mi = typeof(KnownSerializers).GetMethod(nameof(this.GetHandler), BindingFlags.Instance | BindingFlags.NonPublic).MakeGenericMethod(type); + var mi = typeof(KnownSerializers).GetMethod(nameof(this.GetHandler), BindingFlags.Instance | BindingFlags.Public).MakeGenericMethod(type); return (SerializationHandler)mi.Invoke(this, null); } @@ -452,7 +476,7 @@ private SerializationHandler AddHandler() if (!this.knownNames.TryGetValue(type, out string name)) { - name = TypeSchema.GetContractName(type, this.runtimeVersion); + name = TypeSchema.GetContractName(type, this.runtimeInfo.SerializationSystemVersion); } if (!this.schemas.TryGetValue(name, out TypeSchema schema)) diff --git a/Sources/Runtime/Microsoft.Psi/Serialization/MemoryStreamSerializer.cs b/Sources/Runtime/Microsoft.Psi/Serialization/MemoryStreamSerializer.cs index 9b77d5857..1b7edd519 100644 --- a/Sources/Runtime/Microsoft.Psi/Serialization/MemoryStreamSerializer.cs +++ b/Sources/Runtime/Microsoft.Psi/Serialization/MemoryStreamSerializer.cs @@ -12,7 +12,7 @@ namespace Microsoft.Psi.Serialization /// internal sealed class MemoryStreamSerializer : ISerializer { - private const int SchemaVersion = 3; + private const int LatestSchemaVersion = 3; private ISerializer innerSerializer; /// @@ -83,8 +83,16 @@ public TypeSchema Initialize(KnownSerializers serializers, TypeSchema targetSche { var schemaMembers = new[] { new TypeMemberSchema("buffer", typeof(byte[]).AssemblyQualifiedName, true) }; var type = typeof(MemoryStream); - var name = TypeSchema.GetContractName(type, serializers.RuntimeVersion); - var schema = new TypeSchema(name, TypeSchema.GetId(name), type.AssemblyQualifiedName, TypeFlags.IsCollection, schemaMembers, SchemaVersion); + var name = TypeSchema.GetContractName(type, serializers.RuntimeInfo.SerializationSystemVersion); + var schema = new TypeSchema( + type.AssemblyQualifiedName, + TypeFlags.IsCollection, + schemaMembers, + name, + TypeSchema.GetId(name), + LatestSchemaVersion, + this.GetType().AssemblyQualifiedName, + serializers.RuntimeInfo.SerializationSystemVersion); return targetSchema ?? schema; } diff --git a/Sources/Runtime/Microsoft.Psi/Serialization/SimpleArraySerializer.cs b/Sources/Runtime/Microsoft.Psi/Serialization/SimpleArraySerializer.cs index f9b8396cf..273d3f943 100644 --- a/Sources/Runtime/Microsoft.Psi/Serialization/SimpleArraySerializer.cs +++ b/Sources/Runtime/Microsoft.Psi/Serialization/SimpleArraySerializer.cs @@ -15,7 +15,7 @@ namespace Microsoft.Psi.Serialization /// The type of objects this serializer knows how to handle. internal sealed class SimpleArraySerializer : ISerializer { - private const int Version = 2; + private const int LatestSchemaVersion = 2; // for performance reasons, we want serialization to perform block-copy operations over the entire array in one go // however, since this class is generic, the C# compiler doesn't let us get the address of the first element of the array @@ -30,9 +30,17 @@ public TypeSchema Initialize(KnownSerializers serializers, TypeSchema targetSche { serializers.GetHandler(); // register element type var type = typeof(T[]); - var name = TypeSchema.GetContractName(type, serializers.RuntimeVersion); + var name = TypeSchema.GetContractName(type, serializers.RuntimeInfo.SerializationSystemVersion); var elementsMember = new TypeMemberSchema("Elements", typeof(T).AssemblyQualifiedName, true); - var schema = new TypeSchema(name, TypeSchema.GetId(name), type.AssemblyQualifiedName, TypeFlags.IsCollection, new TypeMemberSchema[] { elementsMember }, Version); + var schema = new TypeSchema( + type.AssemblyQualifiedName, + TypeFlags.IsCollection, + new TypeMemberSchema[] { elementsMember }, + name, + TypeSchema.GetId(name), + LatestSchemaVersion, + this.GetType().AssemblyQualifiedName, + serializers.RuntimeInfo.SerializationSystemVersion); return targetSchema ?? schema; } diff --git a/Sources/Runtime/Microsoft.Psi/Serialization/SimpleSerializer.cs b/Sources/Runtime/Microsoft.Psi/Serialization/SimpleSerializer.cs index be1c93e7c..0ac1cd5a3 100644 --- a/Sources/Runtime/Microsoft.Psi/Serialization/SimpleSerializer.cs +++ b/Sources/Runtime/Microsoft.Psi/Serialization/SimpleSerializer.cs @@ -11,7 +11,6 @@ namespace Microsoft.Psi.Serialization /// A primitive type (pure value type). internal sealed class SimpleSerializer : ISerializer { - private const int Version = 0; private SerializeDelegate serializeImpl; private DeserializeDelegate deserializeImpl; @@ -20,7 +19,7 @@ internal sealed class SimpleSerializer : ISerializer public TypeSchema Initialize(KnownSerializers serializers, TypeSchema targetSchema) { - var schema = TypeSchema.FromType(typeof(T), serializers.RuntimeVersion, this.GetType(), Version); + var schema = TypeSchema.FromType(typeof(T), this.GetType().AssemblyQualifiedName, serializers.RuntimeInfo.SerializationSystemVersion); this.serializeImpl = Generator.GenerateSerializeMethod(il => Generator.EmitPrimitiveSerialize(typeof(T), il)); this.deserializeImpl = Generator.GenerateDeserializeMethod(il => Generator.EmitPrimitiveDeserialize(typeof(T), il)); return targetSchema ?? schema; diff --git a/Sources/Runtime/Microsoft.Psi/Serialization/StringArraySerializer.cs b/Sources/Runtime/Microsoft.Psi/Serialization/StringArraySerializer.cs index 7a77b1a74..f7dddea99 100644 --- a/Sources/Runtime/Microsoft.Psi/Serialization/StringArraySerializer.cs +++ b/Sources/Runtime/Microsoft.Psi/Serialization/StringArraySerializer.cs @@ -11,7 +11,7 @@ namespace Microsoft.Psi.Serialization /// internal sealed class StringArraySerializer : ISerializer { - private const int Version = 2; + private const int LatestSchemaVersion = 2; /// public bool? IsClearRequired => false; @@ -20,9 +20,17 @@ public TypeSchema Initialize(KnownSerializers serializers, TypeSchema targetSche { serializers.GetHandler(); // register element type var type = typeof(string[]); - var name = TypeSchema.GetContractName(type, serializers.RuntimeVersion); + var name = TypeSchema.GetContractName(type, serializers.RuntimeInfo.SerializationSystemVersion); var elementsMember = new TypeMemberSchema("Elements", typeof(string).AssemblyQualifiedName, true); - var schema = new TypeSchema(name, TypeSchema.GetId(name), type.AssemblyQualifiedName, TypeFlags.IsCollection, new TypeMemberSchema[] { elementsMember }, Version); + var schema = new TypeSchema( + type.AssemblyQualifiedName, + TypeFlags.IsCollection, + new TypeMemberSchema[] { elementsMember }, + name, + TypeSchema.GetId(name), + LatestSchemaVersion, + this.GetType().AssemblyQualifiedName, + serializers.RuntimeInfo.SerializationSystemVersion); return targetSchema ?? schema; } diff --git a/Sources/Runtime/Microsoft.Psi/Serialization/StringSerializer.cs b/Sources/Runtime/Microsoft.Psi/Serialization/StringSerializer.cs index 5c0e6cd5f..46bec5724 100644 --- a/Sources/Runtime/Microsoft.Psi/Serialization/StringSerializer.cs +++ b/Sources/Runtime/Microsoft.Psi/Serialization/StringSerializer.cs @@ -10,14 +10,12 @@ namespace Microsoft.Psi.Serialization /// internal sealed class StringSerializer : ISerializer { - private const int Version = 0; - /// public bool? IsClearRequired => false; public TypeSchema Initialize(KnownSerializers serializers, TypeSchema targetSchema) { - return targetSchema ?? TypeSchema.FromType(typeof(string), serializers.RuntimeVersion, this.GetType(), Version); + return targetSchema ?? TypeSchema.FromType(typeof(string), this.GetType().AssemblyQualifiedName, serializers.RuntimeInfo.SerializationSystemVersion); } public void Clone(string instance, ref string target, SerializationContext context) diff --git a/Sources/Runtime/Microsoft.Psi/Serialization/StructSerializer.cs b/Sources/Runtime/Microsoft.Psi/Serialization/StructSerializer.cs index 412e5ebfc..461f3120a 100644 --- a/Sources/Runtime/Microsoft.Psi/Serialization/StructSerializer.cs +++ b/Sources/Runtime/Microsoft.Psi/Serialization/StructSerializer.cs @@ -12,8 +12,6 @@ namespace Microsoft.Psi.Serialization /// The value type this serializer knows how to handle. internal sealed class StructSerializer : ISerializer { - private const int Version = 1; - // we use delegates (instead of generating a full class) because dynamic delegates (unlike dynamic types) // can access the private fields of the target type. private SerializeDelegate serializeImpl; @@ -26,7 +24,7 @@ internal sealed class StructSerializer : ISerializer public TypeSchema Initialize(KnownSerializers serializers, TypeSchema targetSchema) { - var runtimeSchema = TypeSchema.FromType(typeof(T), serializers.RuntimeVersion, this.GetType(), Version); + var runtimeSchema = TypeSchema.FromType(typeof(T), this.GetType().AssemblyQualifiedName, serializers.RuntimeInfo.SerializationSystemVersion); var members = runtimeSchema.GetCompatibleMemberSet(targetSchema); this.serializeImpl = Generator.GenerateSerializeMethod(il => Generator.EmitSerializeFields(typeof(T), serializers, il, members)); diff --git a/Sources/Runtime/Microsoft.Psi/Serialization/TypeSchema.cs b/Sources/Runtime/Microsoft.Psi/Serialization/TypeSchema.cs index 6bb066f5e..b79c2585e 100644 --- a/Sources/Runtime/Microsoft.Psi/Serialization/TypeSchema.cs +++ b/Sources/Runtime/Microsoft.Psi/Serialization/TypeSchema.cs @@ -49,7 +49,7 @@ public enum TypeFlags : uint /// public sealed class TypeSchema : Metadata { - private static readonly XsdDataContractExporter DcInspector = new XsdDataContractExporter(); + private static readonly XsdDataContractExporter DcInspector = new (); private Dictionary map; private TypeFlags flags; @@ -58,14 +58,22 @@ public sealed class TypeSchema : Metadata /// /// The contract name. /// The id, as generated by . - /// The assembly-qualified type name. + /// The assembly-qualified name for the data type represented by the schema. /// The type flags. + /// The assembly qualified name of the serializer type. /// The serializable members of the type, in the correct order. - /// The schema version, usually representing the version of the code that generated this schema. - /// The name of the serializer that produced the schema. - /// The version of the serializer that produced the schema. - public TypeSchema(string name, int id, string typeName, TypeFlags flags, IEnumerable members, int version, string serializerTypeName = null, int serializerVersion = 0) - : this(name, id, typeName, version, serializerTypeName, serializerVersion) + /// The schema version. + /// The version of the serialization system. + public TypeSchema( + string typeName, + TypeFlags flags, + IEnumerable members, + string name, + int id, + int version, + string serializerTypeName, + int serializationSystemVersion) + : this (typeName, name, id, version, serializerTypeName, serializationSystemVersion) { this.flags = flags; this.Members = members.ToArray(); @@ -76,11 +84,23 @@ public TypeSchema(string name, int id, string typeName, TypeFlags flags, IEnumer } } - internal TypeSchema(string name, int id, string typeName, int version, string serializerTypeName = null, int serializerVersion = 0) - : base(MetadataKind.TypeSchema, name, id, typeName, version, serializerTypeName, serializerVersion, 0) + internal TypeSchema(string typeName, string name, int id, int version, string serializerTypeName, int serializationSystemVersion) + : base (MetadataKind.TypeSchema, name, id, version, serializationSystemVersion) { + this.TypeName = typeName; + this.SerializerTypeName = serializerTypeName; } + /// + /// Gets the name of the type of data described by the schema. + /// + public string TypeName { get; } + + /// + /// Gets the assembly qualified name of the serializer type. + /// + public string SerializerTypeName { get; } + /// /// Gets the type flags. /// @@ -98,25 +118,45 @@ internal TypeSchema(string name, int id, string typeName, int version, string se /// /// Generates a schema for the specified type. - /// If the type is DataContract-compatible (and version > 0), the schema is based on DataContract rules (see https://docs.microsoft.com/en-us/dotnet/framework/wcf/feature-details/serializable-types?view=netframework-4.7) + /// + /// The type to generate the schema for. + /// The assembly qualified name of the type of the serializer generating the schema. + /// The version of the serialization system used. + /// A schema describing the serialization information for the specified type. + /// + /// The schema version is assigned in this case to the . + /// If you would like to generate a schema with a specific version, use the overload. + /// If the type is DataContract-compatible (and > 0), the schema is based on DataContract rules + /// (see https://docs.microsoft.com/en-us/dotnet/framework/wcf/feature-details/serializable-types?view=netframework-4.7) /// If not, the schema is based on binary serialization rules (see https://docs.microsoft.com/en-us/dotnet/api/system.serializableattribute?view=netframework-4.7). + /// + public static TypeSchema FromType(Type type, string serializerTypeAssemblyQualifiedName, int serializationSystemVersion) + => FromType(type, serializerTypeAssemblyQualifiedName, version: serializationSystemVersion, serializationSystemVersion: serializationSystemVersion); + + /// + /// Generates a schema with a specified version for the specified type. /// /// The type to generate the schema for. - /// The version of the schema generation rules to use (same as ). - /// The type of the serializer that will use this schema. - /// The version of the serializer that will use this schema. + /// The assembly qualified name of the type of the serializer generating the schema. + /// The version for the generated schema. + /// The version of the serialization system used. /// A schema describing the serialization information for the specified type. - public static TypeSchema FromType(Type type, RuntimeInfo runtimeVersion, Type serializer, int serializerVersion) + /// + /// If the type is DataContract-compatible (and > 0), the schema is based on DataContract rules + /// (see https://docs.microsoft.com/en-us/dotnet/framework/wcf/feature-details/serializable-types?view=netframework-4.7) + /// If not, the schema is based on binary serialization rules (see https://docs.microsoft.com/en-us/dotnet/api/system.serializableattribute?view=netframework-4.7). + /// + public static TypeSchema FromType(Type type, string serializerTypeName, int version, int serializationSystemVersion) { // can't support the implicit DataContract model yet (would need to parse the DCInspector schema). // bool hasDataContract = DcInspector.CanExport(type); bool hasDataContract = Attribute.IsDefined(type, typeof(DataContractAttribute)); - string name = GetContractName(type, runtimeVersion); + string name = GetContractName(type, serializationSystemVersion); var members = new List(); TypeFlags flags = type.IsValueType ? TypeFlags.IsStruct : TypeFlags.IsClass; // version 0 didn't use DataContract - if (runtimeVersion.SerializerVersion == 0) + if (serializationSystemVersion == 0) { // code from old version of Generator.cs. // It incorrectly includes inherited fields multiple times, but needs to remain this way for compatibility with v0 stores. @@ -208,19 +248,19 @@ public static TypeSchema FromType(Type type, RuntimeInfo runtimeVersion, Type se } } - return new TypeSchema(name, GetId(name), type.AssemblyQualifiedName, flags, members, runtimeVersion.SerializerVersion, serializer.AssemblyQualifiedName, serializerVersion); + return new TypeSchema(type.AssemblyQualifiedName, flags, members, name, GetId(name), version, serializerTypeName, serializationSystemVersion); } /// /// Returns the contract name for a given type, which is either the DataContract name, if available, or the assembly-qualified type name. /// /// The type to generate the name for. - /// The version of the schema generation rules to use (same as ). + /// The version of the serialization system. /// The DataContract name, if available, or the assembly-qualified type name. - public static string GetContractName(Type type, RuntimeInfo runtimeVersion) + public static string GetContractName(Type type, int serializationSystemVersion) { // v2 will use DcInspector.CanExport(type) - if (Attribute.IsDefined(type, typeof(DataContractAttribute)) && runtimeVersion.SerializerVersion > 0) + if (Attribute.IsDefined(type, typeof(DataContractAttribute)) && serializationSystemVersion > 0) { var name = DcInspector.GetSchemaTypeName(type); if (name != null) @@ -232,18 +272,6 @@ public static string GetContractName(Type type, RuntimeInfo runtimeVersion) return type.AssemblyQualifiedName; } - /// - /// Generates a unique ID for the type, based on the type's contract name - /// (DataContract name, if available, or the assembly-qualified type name). - /// - /// The type to generate an ID for. - /// The version of the schema generation rules to use (same as ). - /// A hash of the type's contract name. - public static int GetId(Type type, RuntimeInfo runtimeVersion) - { - return GetId(GetContractName(type, runtimeVersion)); - } - /// /// Returns a unique ID for the given contract name. /// @@ -255,18 +283,6 @@ public static int GetId(string contractName) return contractName.GetDeterministicHashCode() & 0x3FFFFFFF; } - /// - /// Sets the serializer name and version, which a serializer needs to be able to interpret - /// when initialized with an older schema. - /// - /// The serialize type name. - /// The serializer version. - public void SetSerializerInfo(string serializerType, int serializerVersion) - { - this.SerializerTypeName = serializerType; - this.SerializerVersion = serializerVersion; - } - /// /// Validate whether two schemas are compatible. /// @@ -388,7 +404,17 @@ internal new void Deserialize(BufferReader metadataBuffer) internal override void Serialize(BufferWriter metadataBuffer) { - base.Serialize(metadataBuffer); + // Serialization follows a legacy pattern of fields, as described + // in the comments at the top of the Metadata.Deserialize method. + metadataBuffer.Write(this.Name); + metadataBuffer.Write(this.Id); + metadataBuffer.Write(this.TypeName); + metadataBuffer.Write(this.Version); + metadataBuffer.Write(this.SerializerTypeName); + metadataBuffer.Write(this.SerializationSystemVersion); + metadataBuffer.Write(default(ushort)); // this metadata field is not used by TypeSchema + metadataBuffer.Write((ushort)this.Kind); + if (this.Members == null) { metadataBuffer.Write(0); diff --git a/Sources/Runtime/Test.Psi/SerializationTester.cs b/Sources/Runtime/Test.Psi/SerializationTester.cs index de5f16a70..c990bfc3e 100644 --- a/Sources/Runtime/Test.Psi/SerializationTester.cs +++ b/Sources/Runtime/Test.Psi/SerializationTester.cs @@ -414,7 +414,7 @@ public void DictionaryBackCompat() // Create the known serializers and register the old version of the Dictionary schema. This simulates what would be read from an older store. var serializers = new KnownSerializers(); - var oldSchema = TypeSchema.FromType(typeof(Dictionary), new RuntimeInfo(serializationSystemVersion: 2), typeof(ClassSerializer>), serializerVersion: 1); + var oldSchema = TypeSchema.FromType(typeof(Dictionary), null, serializationSystemVersion: 2); serializers.RegisterSchema(oldSchema); // Deserialize the buffer using a SerializationContext initialized with the old schema @@ -505,14 +505,6 @@ public void SerializeEmitter() Assert.AreEqual(emitter.Name, clonedEmitter.Name); } - [TestMethod] - [Timeout(60000)] - public void SerializePixelFormat() - { - this.ValueTypeCloneTest(System.Drawing.Imaging.PixelFormat.Format24bppRgb); - this.ValueTypeSerializeTest(System.Drawing.Imaging.PixelFormat.Format24bppRgb, new byte[256]); - } - [TestMethod] [Timeout(2000)] public void PocoPerfSerialize() @@ -780,33 +772,24 @@ public void RuntimeInfoTest() Assert.AreEqual(RuntimeInfo.RuntimeName.FullName, runtimeInfo.Name); Assert.AreEqual(0, runtimeInfo.Id); - Assert.AreEqual(default(string), runtimeInfo.TypeName); Assert.AreEqual((RuntimeInfo.RuntimeName.Version.Major << 16) | RuntimeInfo.RuntimeName.Version.Minor, runtimeInfo.Version); - Assert.AreEqual(default(string), runtimeInfo.SerializerTypeName); - Assert.AreEqual(RuntimeInfo.CurrentRuntimeVersion, runtimeInfo.SerializerVersion); - Assert.AreEqual(0, runtimeInfo.CustomFlags); + Assert.AreEqual(RuntimeInfo.LatestSerializationSystemVersion, runtimeInfo.SerializationSystemVersion); Assert.AreEqual(MetadataKind.RuntimeInfo, runtimeInfo.Kind); // Test serialization/deserialization of RuntimeInfo with explicit parameters writer = new BufferWriter(0); new RuntimeInfo( name: "Some Name", - id: 1, - typeName: "Some Type Name", version: 2, - serializerTypeName: "Some Serializer Type Name", - serializerVersion: 7).Serialize(writer); + serializationSystemVersion: 7).Serialize(writer); reader = new BufferReader(writer.Buffer); runtimeInfo = (RuntimeInfo)Metadata.Deserialize(reader); Assert.AreEqual("Some Name", runtimeInfo.Name); - Assert.AreEqual(1, runtimeInfo.Id); - Assert.AreEqual("Some Type Name", runtimeInfo.TypeName); + Assert.AreEqual(0, runtimeInfo.Id); Assert.AreEqual(2, runtimeInfo.Version); - Assert.AreEqual("Some Serializer Type Name", runtimeInfo.SerializerTypeName); - Assert.AreEqual(7, runtimeInfo.SerializerVersion); - Assert.AreEqual(0, runtimeInfo.CustomFlags); + Assert.AreEqual(7, runtimeInfo.SerializationSystemVersion); Assert.AreEqual(MetadataKind.RuntimeInfo, runtimeInfo.Kind); } @@ -828,8 +811,8 @@ public void PsiStreamMetadataTest() writer.Write(123); // ID writer.Write("SomeFakeTypeName"); // TypeName writer.Write(version); // Version - writer.Write("SomeFakeSerializerTypeName"); // SerializerTypeName - writer.Write(7); // SerializerVersion + writer.Write(default(string)); // Not used anymore + writer.Write(7); // SerializationSystemVersion writer.Write((ushort)(isPolymorphic ? StreamMetadataFlags.Polymorphic : 0)); // CustomFlags writer.Write((ushort)MetadataKind.StreamMetadata); // MetadataKind writer.Write(new DateTime(1969, 4, 2)); // OpenedTime @@ -933,8 +916,7 @@ void VerifyMeta(PsiStreamMetadata meta, int version, bool isPolymorphic) Assert.AreEqual(123, meta.Id); Assert.AreEqual(123, meta.Id); Assert.AreEqual("SomeFakeTypeName", meta.TypeName); - Assert.AreEqual("SomeFakeSerializerTypeName", meta.SerializerTypeName); - Assert.AreEqual(7, meta.SerializerVersion); + Assert.AreEqual(7, meta.SerializationSystemVersion); Assert.AreEqual(new DateTime(1969, 4, 2), meta.OpenedTime); Assert.AreEqual(new DateTime(2070, 1, 1), meta.ClosedTime); Assert.AreEqual(messageCount, meta.MessageCount); diff --git a/Sources/Runtime/Test.Psi/SharedTester.cs b/Sources/Runtime/Test.Psi/SharedTester.cs index 9a70e58d7..3bc11e69b 100644 --- a/Sources/Runtime/Test.Psi/SharedTester.cs +++ b/Sources/Runtime/Test.Psi/SharedTester.cs @@ -9,7 +9,6 @@ namespace Test.Psi using System.Threading; using Microsoft.Psi; using Microsoft.Psi.Common; - using Microsoft.Psi.Imaging; using Microsoft.Psi.Serialization; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -331,32 +330,6 @@ public void JoinOfShared() } } - [TestMethod] - [Timeout(60000)] - public void SharedImagePoolCollisionTest() - { - var bmp57 = new System.Drawing.Bitmap(5, 7); - var bmp75 = new System.Drawing.Bitmap(7, 5); - - Assert.AreEqual(5, bmp57.Width); - Assert.AreEqual(7, bmp57.Height); - Assert.AreEqual(7, bmp75.Width); - Assert.AreEqual(5, bmp75.Height); - - var shared57 = ImagePool.GetOrCreateFromBitmap(bmp57); - Assert.AreEqual(5, shared57.Resource.Width); - Assert.AreEqual(7, shared57.Resource.Height); - - // Ensure that the ImagePool is not recycling images based solely on the product of - // width*height (i.e. the same number of pixels but different dimensions), as the - // stride and total size of the recycled image could be incorrect. - - shared57.Dispose(); // release to be recycled - var shared75 = ImagePool.GetOrCreateFromBitmap(bmp75); // should *not* get the recycled image - Assert.AreEqual(7, shared75.Resource.Width); - Assert.AreEqual(5, shared75.Resource.Height); - } - [TestMethod] [Timeout(60000)] public void KeyedSharedPoolTest() diff --git a/Sources/Runtime/Test.Psi/Test.Psi.csproj b/Sources/Runtime/Test.Psi/Test.Psi.csproj index c67b597d8..193b90a7e 100644 --- a/Sources/Runtime/Test.Psi/Test.Psi.csproj +++ b/Sources/Runtime/Test.Psi/Test.Psi.csproj @@ -49,13 +49,11 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - diff --git a/Sources/Spatial/Microsoft.Psi.Spatial.Euclidean.Visualization.Windows/CameraViews/DepthImageCameraViewAsPointCloudVisualizationObject.cs b/Sources/Spatial/Microsoft.Psi.Spatial.Euclidean.Visualization.Windows/CameraViews/DepthImageCameraViewAsPointCloudVisualizationObject.cs index 9d4326726..bc159d7d6 100644 --- a/Sources/Spatial/Microsoft.Psi.Spatial.Euclidean.Visualization.Windows/CameraViews/DepthImageCameraViewAsPointCloudVisualizationObject.cs +++ b/Sources/Spatial/Microsoft.Psi.Spatial.Euclidean.Visualization.Windows/CameraViews/DepthImageCameraViewAsPointCloudVisualizationObject.cs @@ -6,14 +6,19 @@ namespace Microsoft.Psi.Spatial.Euclidean.Visualization using System; using System.Collections.Generic; using System.ComponentModel; + using System.IO; using System.Runtime.Serialization; using System.Threading.Tasks; + using System.Windows; using MathNet.Numerics.LinearAlgebra; using MathNet.Spatial.Euclidean; using Microsoft.Psi.Calibration; using Microsoft.Psi.Imaging; using Microsoft.Psi.Spatial.Euclidean; + using Microsoft.Psi.Visualization; using Microsoft.Psi.Visualization.VisualizationObjects; + using Microsoft.Psi.Visualization.Windows; + using Microsoft.Win32; using Xceed.Wpf.Toolkit.PropertyGrid.Attributes; using Windows = System.Windows.Media.Media3D; @@ -82,6 +87,20 @@ public int Sparsity /// protected override Action Deallocator => data => data.ViewedObject?.Dispose(); + /// + public override List ContextMenuItemsInfo() + { + var items = base.ContextMenuItemsInfo(); + items.Insert( + 0, + new ContextMenuItemInfo( + null, + "Export Point Cloud", + new VisualizationCommand(this.ExportPointCloudToPly), + isEnabled: this.PointCloud.CurrentData != null)); + return items; + } + /// public override void UpdateData() { @@ -184,5 +203,64 @@ private void UpdateVisibility() this.UpdateChildVisibility(this.pointCloud.ModelView, this.Visible && this.CurrentData != default && this.depthImage != null && this.depthImage.Resource != null && this.intrinsics != null && this.position != null); this.UpdateChildVisibility(this.frustum.ModelView, this.Visible && this.CurrentData != default && this.intrinsics != null && this.position != null); } + + private void ExportPointCloudToPly() + { + // Suggest filename of _.ply + string exportFilePath = $"{this.StreamBinding.StreamName}_{this.PointCloud.CurrentOriginatingTime.Ticks}.ply"; + + // Allow user to modify location and name of file + var saveFileDialog = new SaveFileDialog + { + FileName = exportFilePath, + DefaultExt = ".ply", + Filter = "Polygon File Format|*.ply", + }; + + bool? result = saveFileDialog.ShowDialog(Application.Current.MainWindow); + if (result == true) + { + exportFilePath = saveFileDialog.FileName; + + try + { + var points = this.PointCloud.CurrentData; + using var writer = File.CreateText(exportFilePath); + + // Header + writer.WriteLine("ply"); + writer.WriteLine("format ascii 1.0"); + writer.WriteLine($"element vertex {points.Count}"); + writer.WriteLine("property float x"); + writer.WriteLine("property float y"); + writer.WriteLine("property float z"); + writer.WriteLine("end_header"); + + // Vertices + foreach (var point in points) + { + writer.WriteLine($"{point.X} {point.Y} {point.Z}"); + } + + // Show where the file was written + new MessageBoxWindow( + Application.Current.MainWindow, + "Export Point Cloud to PLY File", + $"Point cloud saved to {exportFilePath}", + "Close", + null).ShowDialog(); + } + catch (Exception e) + { + var exception = e.InnerException ?? e; + new MessageBoxWindow( + Application.Current.MainWindow, + "Error", + exception.Message, + "Close", + null).ShowDialog(); + } + } + } } } diff --git a/Sources/Spatial/Microsoft.Psi.Spatial.Euclidean.Visualization.Windows/LinearVelocity3DVisualizationObject.cs b/Sources/Spatial/Microsoft.Psi.Spatial.Euclidean.Visualization.Windows/LinearVelocity3DVisualizationObject.cs index c62fc63a7..a917d1dcf 100644 --- a/Sources/Spatial/Microsoft.Psi.Spatial.Euclidean.Visualization.Windows/LinearVelocity3DVisualizationObject.cs +++ b/Sources/Spatial/Microsoft.Psi.Spatial.Euclidean.Visualization.Windows/LinearVelocity3DVisualizationObject.cs @@ -85,9 +85,9 @@ private void UpdateVelocityVector() { if (this.CurrentData != null) { - if (this.CurrentData.Speed > 0) + if (this.CurrentData.Magnitude > 0) { - var endPoint = this.CurrentData.Origin + this.CurrentData.Vector; + var endPoint = this.CurrentData.Origin + this.CurrentData.Direction; this.velocityArrow.BeginEdit(); this.velocityArrow.Point1 = new Win3D.Point3D(this.CurrentData.Origin.X, this.CurrentData.Origin.Y, this.CurrentData.Origin.Z); @@ -99,7 +99,7 @@ private void UpdateVelocityVector() private void UpdateVisibility() { - this.UpdateChildVisibility(this.velocityArrow, this.Visible && this.CurrentData != null && this.CurrentData.Speed > 0); + this.UpdateChildVisibility(this.velocityArrow, this.Visible && this.CurrentData != null && this.CurrentData.Magnitude > 0); } } } diff --git a/Sources/Spatial/Microsoft.Psi.Spatial.Euclidean.Visualization.Windows/PointCloud3DDictionaryVisualizationObject.cs b/Sources/Spatial/Microsoft.Psi.Spatial.Euclidean.Visualization.Windows/PointCloud3DDictionaryVisualizationObject.cs new file mode 100644 index 000000000..21a16bc37 --- /dev/null +++ b/Sources/Spatial/Microsoft.Psi.Spatial.Euclidean.Visualization.Windows/PointCloud3DDictionaryVisualizationObject.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.Psi.Spatial.Euclidean.Visualization +{ + using System; + using System.Collections.Generic; + using System.Windows.Media; + using Microsoft.Psi.Spatial.Euclidean; + using Microsoft.Psi.Visualization.VisualizationObjects; + + /// + /// Implements a visualization object that can display a dictionary of point clouds. + /// + [VisualizationObject("3D Point Clouds")] + public class PointCloud3DDictionaryVisualizationObject : + ModelVisual3DDictionaryVisualizationObject + { + private readonly Random random = new (); + private readonly Dictionary pointCloudColors = new (); + + /// + public override void UpdateData() + { + base.UpdateData(); + this.BeginUpdate(); + + foreach (var key in this.Keys) + { + this[key].Color = this.GetPointCloudColor(key); + } + + this.EndUpdate(); + } + + private Color GetPointCloudColor(int key) + { + byte RandomChannelValue() + { + return (byte)this.random.Next(64, 192); + } + + if (!this.pointCloudColors.ContainsKey(key)) + { + this.pointCloudColors.Add( + key, + Color.FromRgb( + RandomChannelValue(), + RandomChannelValue(), + RandomChannelValue())); + } + + return this.pointCloudColors[key]; + } + } +} \ No newline at end of file diff --git a/Sources/Spatial/Microsoft.Psi.Spatial.Euclidean/AngularVelocity3D.cs b/Sources/Spatial/Microsoft.Psi.Spatial.Euclidean/AngularVelocity3D.cs index 0ddf6a6da..f31b68412 100644 --- a/Sources/Spatial/Microsoft.Psi.Spatial.Euclidean/AngularVelocity3D.cs +++ b/Sources/Spatial/Microsoft.Psi.Spatial.Euclidean/AngularVelocity3D.cs @@ -6,30 +6,39 @@ namespace Microsoft.Psi.Spatial.Euclidean using System; using MathNet.Numerics.LinearAlgebra; using MathNet.Spatial.Euclidean; + using MathNet.Spatial.Units; + using Microsoft.Psi.Common; + using Microsoft.Psi.Serialization; using static Microsoft.Psi.Calibration.CalibrationExtensions; /// - /// Represents an angular 3D velocity, starting from an original rotation, - /// around a particular axis of rotation. + /// Represents an angular velocity, rotating around a particular axis from a starting rotation. /// + [Serializer(typeof(AngularVelocity3D.CustomSerializer))] public readonly struct AngularVelocity3D : IEquatable { /// - /// The origin of rotation. + /// The starting rotation of origin. /// public readonly Matrix OriginRotation; /// - /// The axis of angular velocity, along with the radians/time speed (length of the vector). + /// The axis of the angular direction of motion for this velocity. /// - public readonly Vector3D AxisAngleVector; + public readonly UnitVector3D AxisDirection; + + /// + /// Gets the magnitude (per-second speed) of the velocity. + /// + public readonly Angle Magnitude; /// /// Initializes a new instance of the struct. /// - /// The origin of rotation. - /// The axis-angle representation of velocity. - public AngularVelocity3D(Matrix originRotation, Vector3D axisAngleVector) + /// The starting rotation. + /// The axis of the angular direction of motion. + /// The magnitude (per-second speed) of the velocity. + public AngularVelocity3D(Matrix originRotation, UnitVector3D axisDirection, Angle magnitude) { if (originRotation.RowCount != 3 || originRotation.ColumnCount != 3) @@ -37,29 +46,25 @@ public AngularVelocity3D(Matrix originRotation, Vector3D axisAngleVector throw new ArgumentException("Rotation matrix must be 3x3."); } - this.OriginRotation = originRotation; - this.AxisAngleVector = axisAngleVector; - } + if (axisDirection == default(UnitVector3D) && magnitude.Radians != 0) + { + throw new ArgumentException("Axis direction cannot be (0,0,0) with a non-zero magnitude."); + } - /// - /// Initializes a new instance of the struct. - /// - /// The origin of rotation. - /// The axis of velocity. - /// The angular speed (radians/time). - public AngularVelocity3D(Matrix originRotation, UnitVector3D axis, double speed) - : this(originRotation, axis.ScaleBy(speed)) - { + this.OriginRotation = originRotation; + this.AxisDirection = axisDirection; + this.Magnitude = magnitude; } /// /// Initializes a new instance of the struct. /// - /// The origin of rotation. + /// The starting rotation. /// A destination rotation. /// The time it took to reach the destination rotation. - /// An optional angle epsilon parameter used to determine when the specified matrix contains a zero-rotation (by default 0.01 degrees). - public AngularVelocity3D(Matrix originRotation, Matrix destinationRotation, TimeSpan time, double angleEpsilon = 0.01 * Math.PI / 180) + /// An optional angle epsilon parameter used to determine when + /// the computed rotation matrix contains a zero-rotation (by default 0.01 degrees). + public AngularVelocity3D(Matrix originRotation, Matrix destinationRotation, TimeSpan time, Angle? angleEpsilon = null) { if (originRotation.RowCount != 3 || originRotation.ColumnCount != 3 || @@ -70,32 +75,11 @@ public AngularVelocity3D(Matrix originRotation, Matrix destinati } this.OriginRotation = originRotation; - var axisAngleDistance = Vector3D.OfVector(MatrixToAxisAngle(destinationRotation * originRotation.Inverse(), angleEpsilon)); - var angularSpeed = axisAngleDistance.Length / time.TotalSeconds; - this.AxisAngleVector = angularSpeed == 0 ? default : axisAngleDistance.Normalize().ScaleBy(angularSpeed); - } - - /// - /// Initializes a new instance of the struct. - /// - /// The origin coordinate system. - /// The destination coordinate system. - /// The time it took to reach the destination coordinate system. - /// An optional angle epsilon parameter used to determine when the specified matrix contains a zero-rotation (by default 0.01 degrees). - public AngularVelocity3D( - CoordinateSystem originCoordinateSystem, - CoordinateSystem destinationCoordinateSystem, - TimeSpan time, - double angleEpsilon = 0.01 * Math.PI / 180) - : this(originCoordinateSystem.GetRotationSubMatrix(), destinationCoordinateSystem.GetRotationSubMatrix(), time, angleEpsilon) - { + var axisVelocity = Vector3D.OfVector(MatrixToAxisAngle(destinationRotation * originRotation.Inverse(), angleEpsilon)); + this.Magnitude = Angle.FromRadians(axisVelocity.Length / time.TotalSeconds); + this.AxisDirection = axisVelocity.Length < float.Epsilon ? default : axisVelocity.Normalize(); } - /// - /// Gets the magnitude of the velocity. - /// - public double Speed => this.AxisAngleVector.Length; - /// /// Returns a value indicating whether the specified velocities are the same. /// @@ -113,25 +97,78 @@ public AngularVelocity3D(Matrix originRotation, Matrix destinati public static bool operator !=(AngularVelocity3D left, AngularVelocity3D right) => !left.Equals(right); /// - public bool Equals(AngularVelocity3D other) => this.OriginRotation.Equals(other.OriginRotation) && this.AxisAngleVector == other.AxisAngleVector; + public bool Equals(AngularVelocity3D other) => + this.OriginRotation.Equals(other.OriginRotation) && + this.AxisDirection.Equals(other.AxisDirection) && + this.Magnitude.Equals(other.Magnitude); /// public override bool Equals(object obj) => obj is AngularVelocity3D other && this.Equals(other); /// - public override int GetHashCode() => HashCode.Combine(this.OriginRotation, this.AxisAngleVector); + public override int GetHashCode() => HashCode.Combine(this.OriginRotation, this.AxisDirection, this.Magnitude); /// /// Computes the destination rotation, if this velocity is followed for a given amount of time. /// /// The span of time to compute over. /// The destination rotation. - /// The unit of time should be the same as assumed for the axis-angle velocity vector (e.g., seconds). - public Matrix ComputeDestination(double time) + public Matrix ComputeDestination(TimeSpan time) => + AxisAngleToMatrix(this.AxisDirection.ScaleBy(this.Magnitude.Radians * time.TotalSeconds).ToVector()) * this.OriginRotation; + + /// + /// Provides custom read- backcompat serialization for objects. + /// + public class CustomSerializer : BackCompatStructSerializer { - var angularDistance = this.AxisAngleVector.Length * time; - var axisAngleDistance = this.AxisAngleVector.Normalize().ScaleBy(angularDistance); - return AxisAngleToMatrix(axisAngleDistance.ToVector()) * this.OriginRotation; + // When introducing a custom serializer, the LatestSchemaVersion + // is set to be one above the auto-generated schema version (given by + // RuntimeInfo.LatestSerializationSystemVersion, which was 2 at the time) + private const int LatestSchemaVersion = 3; + private SerializationHandler> matrixHandler; + private SerializationHandler vector3DHandler; + + /// + /// Initializes a new instance of the class. + /// + public CustomSerializer() + : base(LatestSchemaVersion) + { + } + + /// + public override void InitializeBackCompatSerializationHandlers(int schemaVersion, KnownSerializers serializers, TypeSchema targetSchema) + { + if (schemaVersion <= 2) + { + this.matrixHandler = serializers.GetHandler>(); + this.vector3DHandler = serializers.GetHandler(); + } + else + { + throw new NotSupportedException($"{nameof(AngularVelocity3D.CustomSerializer)} only supports schema versions 2 and 3."); + } + } + + /// + public override void BackCompatDeserialize(int schemaVersion, BufferReader reader, ref AngularVelocity3D target, SerializationContext context) + { + if (schemaVersion <= 2) + { + var originRotation = default(Matrix); + var axisAngleVector = default(Vector3D); + this.matrixHandler.Deserialize(reader, ref originRotation, context); + this.vector3DHandler.Deserialize(reader, ref axisAngleVector, context); + target = new AngularVelocity3D( + originRotation, + axisAngleVector.Length >= float.Epsilon ? axisAngleVector.Normalize() : default, + Angle.FromRadians(axisAngleVector.Length)); + } + else + { + throw new NotSupportedException($"{nameof(AngularVelocity3D.CustomSerializer)} only supports schema versions 2 and 3."); + } + } } } } diff --git a/Sources/Spatial/Microsoft.Psi.Spatial.Euclidean/Bounds3D.cs b/Sources/Spatial/Microsoft.Psi.Spatial.Euclidean/Bounds3D.cs index 25a418f00..f14bf2f8f 100644 --- a/Sources/Spatial/Microsoft.Psi.Spatial.Euclidean/Bounds3D.cs +++ b/Sources/Spatial/Microsoft.Psi.Spatial.Euclidean/Bounds3D.cs @@ -60,6 +60,11 @@ public Bounds3D(double x1, double x2, double y1, double y2, double z1, double z2 /// public Point3D Center => Point3D.MidPoint(this.Min.ToPoint3D(), this.Max.ToPoint3D()); + /// + /// Gets the volume of the bounds. + /// + public double Volume => this.SizeX * this.SizeY * this.SizeZ; + /// /// Gets a value indicating whether the bounds are degenerate /// (i.e. size zero in one or more dimensions). diff --git a/Sources/Spatial/Microsoft.Psi.Spatial.Euclidean/Box3D.cs b/Sources/Spatial/Microsoft.Psi.Spatial.Euclidean/Box3D.cs index 99506e3ed..52d185498 100644 --- a/Sources/Spatial/Microsoft.Psi.Spatial.Euclidean/Box3D.cs +++ b/Sources/Spatial/Microsoft.Psi.Spatial.Euclidean/Box3D.cs @@ -164,6 +164,11 @@ public Box3D(Point3D origin, UnitVector3D xAxis, UnitVector3D yAxis, UnitVector3 /// public double LengthZ => this.Bounds.SizeZ; + /// + /// Gets the volume of the box. + /// + public double Volume => this.Bounds.Volume; + /// /// Gets a value indicating whether the box is degenerate /// (i.e. one or more of its edges has zero length). diff --git a/Sources/Spatial/Microsoft.Psi.Spatial.Euclidean/CoordinateSystemVelocity3D.cs b/Sources/Spatial/Microsoft.Psi.Spatial.Euclidean/CoordinateSystemVelocity3D.cs index 389ca9523..209d6b4fa 100644 --- a/Sources/Spatial/Microsoft.Psi.Spatial.Euclidean/CoordinateSystemVelocity3D.cs +++ b/Sources/Spatial/Microsoft.Psi.Spatial.Euclidean/CoordinateSystemVelocity3D.cs @@ -5,60 +5,54 @@ namespace Microsoft.Psi.Spatial.Euclidean { using System; using MathNet.Spatial.Euclidean; - using static Microsoft.Psi.Calibration.CalibrationExtensions; + using MathNet.Spatial.Units; + using Microsoft.Psi.Common; + using Microsoft.Psi.Serialization; /// - /// Represents a coordinate system velocity from a particular starting pose, - /// composing both a linear and angular velocity. + /// Represents a coordinate system velocity composing both a linear and angular velocity. /// + [Serializer(typeof(CoordinateSystemVelocity3D.CustomSerializer))] public readonly struct CoordinateSystemVelocity3D : IEquatable { /// - /// The origin coordinate system. + /// The angular velocity corresponding to the speed and direction of rotation. /// - public readonly CoordinateSystem OriginCoordinateSystem; + public readonly AngularVelocity3D Angular; /// - /// The axis of angular velocity, along with the radians/time speed (length of the vector). + /// The linear velocity corresponding to the speed and direction of translation. /// - public readonly Vector3D AxisAngleVector; - - /// - /// The linear velocity vector. Describes the direction of motion as well as the speed (length of the vector). - /// - public readonly Vector3D LinearVector; + public readonly LinearVelocity3D Linear; /// /// Initializes a new instance of the struct. /// - /// The origin coordinate system. - /// The axis-angle representation of angular velocity. - /// The linear velocity vector. - public CoordinateSystemVelocity3D( - CoordinateSystem originCoordinateSystem, - Vector3D axisAngleVector, - Vector3D linearVector) + /// The angular velocity component. + /// The linear velocity component. + public CoordinateSystemVelocity3D(AngularVelocity3D angularVelocity, LinearVelocity3D linearVelocity) { - this.OriginCoordinateSystem = originCoordinateSystem; - this.AxisAngleVector = axisAngleVector; - this.LinearVector = linearVector; + this.Angular = angularVelocity; + this.Linear = linearVelocity; } /// /// Initializes a new instance of the struct. /// - /// The origin coordinate system. - /// The axis of angular velocity. - /// The angular speed around the axis. + /// The starting coordinate system. + /// The axis direction of rotation for the angular velocity. + /// The magnitude (per-second speed) of the angular velocity. /// The direction of linear velocity. - /// The linear speed. + /// The magnitude (per-second speed) of the linear velocity. public CoordinateSystemVelocity3D( CoordinateSystem originCoordinateSystem, - UnitVector3D angularAxis, - double angularSpeed, + UnitVector3D rotationAxisDirection, + Angle angularVelocityMagnitude, UnitVector3D linearDirection, - double linearSpeed) - : this(originCoordinateSystem, angularAxis.ScaleBy(angularSpeed), linearDirection.ScaleBy(linearSpeed)) + double linearVelocityMagnitude) + : this( + new AngularVelocity3D(originCoordinateSystem.GetRotationSubMatrix(), rotationAxisDirection, angularVelocityMagnitude), + new LinearVelocity3D(originCoordinateSystem.Origin, linearDirection, linearVelocityMagnitude)) { } @@ -68,20 +62,16 @@ namespace Microsoft.Psi.Spatial.Euclidean /// The origin coordinate system. /// A destination coordinate system. /// The time it took to reach the destination coordinate system. - /// An optional angle epsilon parameter used to determine when the specified matrix contains a zero-rotation (by default 0.01 degrees). + /// An optional angle epsilon parameter used to determine when + /// the computed rotation matrix contains a zero-rotation (by default 0.01 degrees). public CoordinateSystemVelocity3D( CoordinateSystem originCoordinateSystem, CoordinateSystem destinationCoordinateSystem, TimeSpan time, - double angleEpsilon = 0.01 * Math.PI / 180) + Angle? angleEpsilon = null) { - this.OriginCoordinateSystem = originCoordinateSystem; - var coordinateDifference = destinationCoordinateSystem.TransformBy(originCoordinateSystem.Invert()); - var timeInSeconds = time.TotalSeconds; - this.LinearVector = coordinateDifference.Origin.ToVector3D().ScaleBy(1.0 / timeInSeconds); - var axisAngleDistance = Vector3D.OfVector(MatrixToAxisAngle(coordinateDifference.GetRotationSubMatrix(), angleEpsilon)); - var angularSpeed = axisAngleDistance.Length / timeInSeconds; - this.AxisAngleVector = angularSpeed == 0 ? default : axisAngleDistance.Normalize().ScaleBy(angularSpeed); + this.Angular = new AngularVelocity3D(originCoordinateSystem.GetRotationSubMatrix(), destinationCoordinateSystem.GetRotationSubMatrix(), time, angleEpsilon); + this.Linear = new LinearVelocity3D(originCoordinateSystem.Origin, destinationCoordinateSystem.Origin, time); } /// @@ -101,37 +91,80 @@ namespace Microsoft.Psi.Spatial.Euclidean public static bool operator !=(CoordinateSystemVelocity3D left, CoordinateSystemVelocity3D right) => !left.Equals(right); /// - public bool Equals(CoordinateSystemVelocity3D other) => this.OriginCoordinateSystem.Equals(other.OriginCoordinateSystem) && this.AxisAngleVector == other.AxisAngleVector && this.LinearVector == other.LinearVector; + public bool Equals(CoordinateSystemVelocity3D other) => this.Angular.Equals(other.Angular) && this.Linear.Equals(other.Linear); /// public override bool Equals(object obj) => obj is CoordinateSystemVelocity3D other && this.Equals(other); /// - public override int GetHashCode() => HashCode.Combine(this.OriginCoordinateSystem, this.AxisAngleVector, this.LinearVector); - - /// - /// Get the linear velocity component of this coordinate system velocity. - /// - /// The linear velocity. - public LinearVelocity3D GetLinearVelocity() => new (this.OriginCoordinateSystem.Origin, this.LinearVector); - - /// - /// Get the angular velocity component of this coordinate system velocity. - /// - /// The angular velocity. - public AngularVelocity3D GetAngularVelocity() => new (this.OriginCoordinateSystem.GetRotationSubMatrix(), this.AxisAngleVector); + public override int GetHashCode() => HashCode.Combine(this.Angular, this.Linear); /// /// Computes the destination coordinate system, if this velocity is followed for a given amount of time. /// /// The span of time to compute over. /// The destination coordinate system. - /// The unit of time should be the same as assumed for the linear and angular velocity vector (e.g., seconds). - public CoordinateSystem ComputeDestination(double time) + public CoordinateSystem ComputeDestination(TimeSpan time) => + CoordinateSystem.Translation(this.Linear.ComputeDestination(time).ToVector3D()) + .SetRotationSubMatrix(this.Angular.ComputeDestination(time)); + + /// + /// Provides custom read- backcompat serialization for objects. + /// + public class CustomSerializer : BackCompatStructSerializer { - var destinationPoint = this.GetLinearVelocity().ComputeDestination(time); - var destinationRotation = this.GetAngularVelocity().ComputeDestination(time); - return new CoordinateSystem(destinationPoint, UnitVector3D.XAxis, UnitVector3D.YAxis, UnitVector3D.ZAxis).SetRotationSubMatrix(destinationRotation); + // When introducing a custom serializer, the LatestSchemaVersion + // is set to be one above the auto-generated schema version (given by + // RuntimeInfo.LatestSerializationSystemVersion, which was 2 at the time) + private const int LatestSchemaVersion = 3; + private SerializationHandler coordinateSystemHandler; + private SerializationHandler vector3DHandler; + + /// + /// Initializes a new instance of the class. + /// + public CustomSerializer() + : base(LatestSchemaVersion) + { + } + + /// + public override void InitializeBackCompatSerializationHandlers(int schemaVersion, KnownSerializers serializers, TypeSchema targetSchema) + { + if (schemaVersion <= 2) + { + this.coordinateSystemHandler = serializers.GetHandler(); + this.vector3DHandler = serializers.GetHandler(); + } + else + { + throw new NotSupportedException($"{nameof(CoordinateSystemVelocity3D.CustomSerializer)} only supports schema versions 2 and 3."); + } + } + + /// + public override void BackCompatDeserialize(int schemaVersion, BufferReader reader, ref CoordinateSystemVelocity3D target, SerializationContext context) + { + if (schemaVersion <= 2) + { + var originCoordinateSystem = default(CoordinateSystem); + var axisAngleVector = default(Vector3D); + var linearVector = default(Vector3D); + this.coordinateSystemHandler.Deserialize(reader, ref originCoordinateSystem, context); + this.vector3DHandler.Deserialize(reader, ref axisAngleVector, context); + this.vector3DHandler.Deserialize(reader, ref linearVector, context); + target = new CoordinateSystemVelocity3D( + originCoordinateSystem ?? new CoordinateSystem(), + axisAngleVector.Length >= float.Epsilon ? axisAngleVector.Normalize() : default, + Angle.FromRadians(axisAngleVector.Length), + linearVector.Length >= float.Epsilon ? linearVector.Normalize() : default, + linearVector.Length); + } + else + { + throw new NotSupportedException($"{nameof(CoordinateSystemVelocity3D.CustomSerializer)} only supports schema versions 2 and 3."); + } + } } } } diff --git a/Sources/Spatial/Microsoft.Psi.Spatial.Euclidean/LinearVelocity3D.cs b/Sources/Spatial/Microsoft.Psi.Spatial.Euclidean/LinearVelocity3D.cs index 6d64f9fe1..4543c4161 100644 --- a/Sources/Spatial/Microsoft.Psi.Spatial.Euclidean/LinearVelocity3D.cs +++ b/Sources/Spatial/Microsoft.Psi.Spatial.Euclidean/LinearVelocity3D.cs @@ -5,63 +5,62 @@ namespace Microsoft.Psi.Spatial.Euclidean { using System; using MathNet.Spatial.Euclidean; + using Microsoft.Psi.Common; + using Microsoft.Psi.Serialization; /// - /// Represents a linear 3D velocity rooted at a point in space. + /// Represents a linear velocity from a starting point in 3D space. /// + [Serializer(typeof(LinearVelocity3D.CustomSerializer))] public readonly struct LinearVelocity3D : IEquatable { /// - /// The point of origin. + /// The starting point of origin. /// public readonly Point3D Origin; /// - /// The velocity vector. Describes the direction of motion as well as the speed (length of the vector). + /// The vector direction of motion of the velocity. /// - public readonly Vector3D Vector; + public readonly UnitVector3D Direction; /// - /// Initializes a new instance of the struct. + /// The scalar magnitude (per-second speed) of the velocity. /// - /// The origin point. - /// The velocity vector. - public LinearVelocity3D(Point3D origin, Vector3D vector) - { - this.Origin = origin; - this.Vector = vector; - } + public readonly double Magnitude; /// /// Initializes a new instance of the struct. /// - /// The origin of the velocity. - /// The unit vector indicating the direction of velocity. - /// The speed in the specified direction. - public LinearVelocity3D(Point3D origin, UnitVector3D unitVector, double speed) - : this(origin, unitVector.ScaleBy(speed)) + /// The starting point of origin. + /// The unit vector indicating the direction of velocity. + /// The scalar magnitude (per-second speed) of the velocity. + public LinearVelocity3D(Point3D origin, UnitVector3D direction, double magnitude) { + if (direction == default(UnitVector3D) && magnitude != 0) + { + throw new ArgumentException("Axis direction cannot be (0,0,0) with a non-zero magnitude."); + } + + this.Origin = origin; + this.Direction = direction; + this.Magnitude = magnitude; } /// /// Initializes a new instance of the struct. /// - /// The origin of the velocity. + /// The starting point of origin. /// A destination point. - /// The time it took to reach that destination point. + /// The time taken to reach that destination point. public LinearVelocity3D(Point3D origin, Point3D destinationPoint, TimeSpan time) { this.Origin = origin; var directionVector = destinationPoint - origin; - var speed = directionVector.Length / time.TotalSeconds; - this.Vector = directionVector.Normalize().ScaleBy(speed); + this.Magnitude = directionVector.Length / time.TotalSeconds; + this.Direction = directionVector.Length >= float.Epsilon ? directionVector.Normalize() : default; } - /// - /// Gets the magnitude of the velocity. - /// - public double Speed => this.Vector.Length; - /// /// Returns a value indicating whether the specified velocities are the same. /// @@ -79,23 +78,78 @@ public LinearVelocity3D(Point3D origin, Point3D destinationPoint, TimeSpan time) public static bool operator !=(LinearVelocity3D left, LinearVelocity3D right) => !left.Equals(right); /// - public bool Equals(LinearVelocity3D other) => this.Origin == other.Origin && this.Vector == other.Vector; + public bool Equals(LinearVelocity3D other) => + this.Origin.Equals(other.Origin) && + this.Direction.Equals(other.Direction) && + this.Magnitude.Equals(other.Magnitude); /// public override bool Equals(object obj) => obj is LinearVelocity3D other && this.Equals(other); /// - public override int GetHashCode() => HashCode.Combine(this.Origin, this.Vector); + public override int GetHashCode() => HashCode.Combine(this.Origin, this.Direction, this.Magnitude); /// /// Computes the destination point, if this velocity is followed for a given amount of time. /// /// The span of time to compute over. /// The destination point. - /// The unit of time should be the same as assumed for the velocity vector (e.g., seconds). - public Point3D ComputeDestination(double time) + public Point3D ComputeDestination(TimeSpan time) => + this.Origin + this.Direction.ScaleBy(this.Magnitude * time.TotalSeconds); + + /// + /// Provides backcompat serialization for objects. + /// + public class CustomSerializer : BackCompatStructSerializer { - return this.Origin + this.Vector.ScaleBy(time); + // When introducing a custom serializer, the LatestSchemaVersion + // is set to be one above the auto-generated schema version (given by + // RuntimeInfo.LatestSerializationSystemVersion, which was 2 at the time) + private const int LatestSchemaVersion = 3; + private SerializationHandler point3DHandler; + private SerializationHandler vector3DHandler; + + /// + /// Initializes a new instance of the class. + /// + public CustomSerializer() + : base(LatestSchemaVersion) + { + } + + /// + public override void InitializeBackCompatSerializationHandlers(int schemaVersion, KnownSerializers serializers, TypeSchema targetSchema) + { + if (schemaVersion <= 2) + { + this.point3DHandler = serializers.GetHandler(); + this.vector3DHandler = serializers.GetHandler(); + } + else + { + throw new NotSupportedException($"{nameof(LinearVelocity3D.CustomSerializer)} only supports schema versions 2 and 3."); + } + } + + /// + public override void BackCompatDeserialize(int schemaVersion, BufferReader reader, ref LinearVelocity3D target, SerializationContext context) + { + if (schemaVersion <= 2) + { + var origin = default(Point3D); + var vector = default(Vector3D); + this.point3DHandler.Deserialize(reader, ref origin, context); + this.vector3DHandler.Deserialize(reader, ref vector, context); + target = new LinearVelocity3D( + origin, + vector.Length >= float.Epsilon ? vector.Normalize() : default, + vector.Length); + } + else + { + throw new NotSupportedException($"{nameof(LinearVelocity3D.CustomSerializer)} only supports schema versions 2 and 3."); + } + } } } } diff --git a/Sources/Spatial/Microsoft.Psi.Spatial.Euclidean/Operators.cs b/Sources/Spatial/Microsoft.Psi.Spatial.Euclidean/Operators.cs index 6188aae7c..552a90fcd 100644 --- a/Sources/Spatial/Microsoft.Psi.Spatial.Euclidean/Operators.cs +++ b/Sources/Spatial/Microsoft.Psi.Spatial.Euclidean/Operators.cs @@ -4,6 +4,7 @@ namespace Microsoft.Psi.Spatial.Euclidean { using System; + using System.Linq; using System.Numerics; using MathNet.Numerics.LinearAlgebra; using MathNet.Spatial.Euclidean; @@ -91,28 +92,42 @@ public static Box3D Transform(this CoordinateSystem coordinateSystem, Box3D box3 } /// - /// Gets a rotation matrix corresponding to a forward vector. + /// Computes the velocity (with linear and angular components) of streaming poses. /// - /// The specified forward vector. - /// The corresponding rotation matrix. - /// The X axis of the matrix will correspond to the specified forward vector. - public static Matrix ToRotationMatrix(this Vector3D forward) => forward.Normalize().ToRotationMatrix(); + /// The source stream of poses. + /// An optional delivery policy parameter. + /// An optional name for the stream operator. + /// A stream containing the computed velocities. + public static IProducer ComputeVelocity( + this IProducer source, + DeliveryPolicy deliveryPolicy = null, + string name = nameof(ComputeVelocity)) + => source.Window( + -1, + 0, + values => new CoordinateSystemVelocity3D(values.First().Data, values.Last().Data, values.Last().OriginatingTime - values.First().OriginatingTime), + deliveryPolicy, + name); /// /// Gets a rotation matrix corresponding to a forward vector. /// - /// The specified forward vector. + /// The specified forward vector. + /// Optional world "up" vector to use as reference when computing the rotation. + /// Defaults to . /// The corresponding rotation matrix. /// The X axis of the matrix will correspond to the specified forward vector. - public static Matrix ToRotationMatrix(this UnitVector3D forward) + public static Matrix ToRotationMatrix(this UnitVector3D forwardVector3D, UnitVector3D? worldUpVector3D = null) { - // Compute left and up directions from the given forward direction. - var left = UnitVector3D.ZAxis.CrossProduct(forward); - var up = forward.CrossProduct(left); + worldUpVector3D ??= UnitVector3D.ZAxis; + + // Compute local left and up vectors from the given forward vector and world up vector. + var left = worldUpVector3D.Value.CrossProduct(forwardVector3D); + var up = forwardVector3D.CrossProduct(left); // Create a corresponding 3x3 matrix from the 3 directions. var rotationMatrix = Matrix.Build.Dense(3, 3); - rotationMatrix.SetColumn(0, forward.ToVector()); + rotationMatrix.SetColumn(0, forwardVector3D.ToVector()); rotationMatrix.SetColumn(1, left.ToVector()); rotationMatrix.SetColumn(2, up.ToVector()); return rotationMatrix; @@ -177,16 +192,17 @@ public static CoordinateSystem ToCoordinateSystem(this Matrix4x4 matrix) /// /// The first . /// The second . - /// The amount to interpolate between the two coordinate systems. A value of 0 will - /// effectively return the first coordinate system, a value of 1 will effectively return the second - /// coordinate system, and a value between 0 and 1 will return an interpolation between those two values. + /// The amount to interpolate between the two coordinate systems. + /// A value between 0 and 1 will return an interpolation between the two values. /// A value outside the 0-1 range will generate an extrapolated result. /// The interpolated . + /// Returns null if either input value is null. public static CoordinateSystem InterpolateCoordinateSystems(CoordinateSystem cs1, CoordinateSystem cs2, double amount) { - // We assume an identity coordinate system by default if either input happens to be null - cs1 ??= new CoordinateSystem(); - cs2 ??= new CoordinateSystem(); + if (cs1 is null || cs2 is null) + { + return null; + } // Extract translation as vectors var t1 = cs1.Origin.ToVector3D(); diff --git a/Sources/Spatial/Microsoft.Psi.Spatial.Euclidean/PointCloud3D.cs b/Sources/Spatial/Microsoft.Psi.Spatial.Euclidean/PointCloud3D.cs index 245a95ed9..df122680c 100644 --- a/Sources/Spatial/Microsoft.Psi.Spatial.Euclidean/PointCloud3D.cs +++ b/Sources/Spatial/Microsoft.Psi.Spatial.Euclidean/PointCloud3D.cs @@ -79,6 +79,20 @@ private PointCloud3D() /// public int NumberOfPoints => this.points != null ? this.points.ColumnCount : 0; + /// + /// Gets the centroid of the point cloud. + /// + public Point3D Centroid + { + get + { + var x = this.points.Row(0).Average(); + var y = this.points.Row(1).Average(); + var z = this.points.Row(2).Average(); + return new Point3D(x, y, z); + } + } + /// /// Create a point cloud from a shared depth image. /// diff --git a/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/MainWindow.xaml b/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/MainWindow.xaml index 71dd771ee..b7537388c 100644 --- a/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/MainWindow.xaml +++ b/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/MainWindow.xaml @@ -15,17 +15,19 @@ xmlns:xctk="clr-namespace:Xceed.Wpf.Toolkit.PropertyGrid;assembly=Xceed.Wpf.Toolkit" xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity" Title="{Binding TitleText}" - WindowState="{Binding AppSettings.WindowState, Mode=TwoWay}" - Top="{Binding AppSettings.WindowPositionTop, Mode=TwoWay}" - Left="{Binding AppSettings.WindowPositionLeft, Mode=TwoWay}" - Height="{Binding AppSettings.WindowHeight, Mode=TwoWay}" - Width="{Binding AppSettings.WindowWidth, Mode=TwoWay}" + WindowState="{Binding Settings.WindowState, Mode=TwoWay}" + Top="{Binding Settings.WindowPositionTop, Mode=TwoWay}" + Left="{Binding Settings.WindowPositionLeft, Mode=TwoWay}" + Height="{Binding Settings.WindowHeight, Mode=TwoWay}" + Width="{Binding Settings.WindowWidth, Mode=TwoWay}" Background="{StaticResource WindowBackgroundBrush}"> - + + + @@ -68,14 +70,32 @@ + + + + + + - - + + + @@ -120,10 +140,10 @@ - - - + - + - + - + - + @@ -311,7 +331,7 @@ - + @@ -352,7 +372,7 @@ - + diff --git a/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/MainWindow.xaml.cs b/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/MainWindow.xaml.cs index be0066f02..4f57f2004 100644 --- a/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/MainWindow.xaml.cs +++ b/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/MainWindow.xaml.cs @@ -37,18 +37,18 @@ public MainWindow() { this.InitializeComponent(); - // Create the context - var viewModel = new MainWindowViewModel(); - // Create the visualization container and set the navigator range to an arbitrary default VisualizationContext visualizationContext = VisualizationContext.Instance; visualizationContext.VisualizationContainer = new VisualizationContainer(); visualizationContext.VisualizationContainer.Navigator.ViewRange.Set(DateTime.UtcNow, TimeSpan.FromSeconds(60)); + // Create the context + var viewModel = new MainWindowViewModel(); + // Set the values for the timing buttons on the navigator - visualizationContext.VisualizationContainer.Navigator.ShowAbsoluteTiming = viewModel.AppSettings.ShowAbsoluteTiming; - visualizationContext.VisualizationContainer.Navigator.ShowTimingRelativeToSessionStart = viewModel.AppSettings.ShowTimingRelativeToSessionStart; - visualizationContext.VisualizationContainer.Navigator.ShowTimingRelativeToSelectionStart = viewModel.AppSettings.ShowTimingRelativeToSelectionStart; + visualizationContext.VisualizationContainer.Navigator.ShowAbsoluteTiming = viewModel.Settings.ShowAbsoluteTiming; + visualizationContext.VisualizationContainer.Navigator.ShowTimingRelativeToSessionStart = viewModel.Settings.ShowTimingRelativeToSessionStart; + visualizationContext.VisualizationContainer.Navigator.ShowTimingRelativeToSelectionStart = viewModel.Settings.ShowTimingRelativeToSelectionStart; // Set the data context this.DataContext = viewModel; diff --git a/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/MainWindowViewModel.cs b/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/MainWindowViewModel.cs index cea31dc35..e9f7bc327 100644 --- a/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/MainWindowViewModel.cs +++ b/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/MainWindowViewModel.cs @@ -7,6 +7,7 @@ namespace Microsoft.Psi.PsiStudio using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; + using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.Serialization; @@ -19,7 +20,6 @@ namespace Microsoft.Psi.PsiStudio using Microsoft.Psi.PsiStudio.Windows; using Microsoft.Psi.Visualization; using Microsoft.Psi.Visualization.Data; - using Microsoft.Psi.Visualization.Helpers; using Microsoft.Psi.Visualization.Navigation; using Microsoft.Psi.Visualization.ViewModels; using Microsoft.Psi.Visualization.VisualizationObjects; @@ -47,9 +47,15 @@ public class MainWindowViewModel : ObservableObject /// private static readonly string PsiStudioAnnotationSchemasPath = Path.Combine(PsiStudioDocumentsPath, "AnnotationSchemas"); + /// + /// The path to the batch processing task configurations directory. + /// + private static readonly string PsiStudioBatchProcessingTaskConfigurationsPath = Path.Combine(PsiStudioDocumentsPath, "BatchProcessingTaskConfigurations"); + private readonly TimeSpan nudgeTimeSpan = TimeSpan.FromSeconds(1 / 30.0); private readonly TimeSpan jumpTimeSpan = TimeSpan.FromSeconds(1 / 6.0); private readonly string newLayoutName = ""; + private readonly Dictionary userConsentObtained = new (); private List availableLayouts = new (); private List annotationSchemas; private LayoutInfo currentLayout = null; @@ -77,13 +83,14 @@ public class MainWindowViewModel : ObservableObject private RelayCommand playPauseCommand; private RelayCommand goToTimeCommand; - private RelayCommand toggleCursorFollowsMouseComand; + private RelayCommand toggleCursorFollowsMouseCommand; private RelayCommand nudgeRightCommand; private RelayCommand nudgeLeftCommand; private RelayCommand jumpRightCommand; private RelayCommand jumpLeftCommand; private RelayCommand openStoreCommand; private RelayCommand openDatasetCommand; + private RelayCommand openRecentlyUsedDatasetCommand; private RelayCommand saveDatasetAsCommand; private RelayCommand insertTimelinePanelCommand; private RelayCommand insert1CellInstantPanelCommand; @@ -115,8 +122,8 @@ public class MainWindowViewModel : ObservableObject private RelayCommand treeSelectedCommand; private RelayCommand closedCommand; private RelayCommand exitCommand; - - ////private RelayCommand showSettingsWindowComand; + private RelayCommand editSettingsCommand; + private RelayCommand viewAdditionalAssemblyLoadErrorLogCommand; /// /// Initializes a new instance of the class. @@ -124,7 +131,7 @@ public class MainWindowViewModel : ObservableObject public MainWindowViewModel() { // Create and load the settings - this.AppSettings = PsiStudioSettings.Load(Path.Combine(PsiStudioDocumentsPath, "PsiStudioSettings.xml")); + this.Settings = PsiStudioSettings.Load(Path.Combine(PsiStudioDocumentsPath, "PsiStudioSettings.xml")); // Wait until the main window is visible before initializing the visualizer // map as we may need to display some message boxes during this process. @@ -142,6 +149,9 @@ public MainWindowViewModel() // Load the available layouts this.UpdateLayoutList(); + + // Listen for navigator property changes to capture in settings + this.VisualizationContainer.Navigator.PropertyChanged += this.OnNavigatorPropertyChanged; } /// @@ -152,7 +162,7 @@ public MainWindowViewModel() /// /// Gets the application settings. /// - public PsiStudioSettings AppSettings { get; private set; } + public PsiStudioSettings Settings { get; } /// /// Gets the visualization container. @@ -221,8 +231,8 @@ public RelayCommand GoToTimeCommand /// [Browsable(false)] [IgnoreDataMember] - public RelayCommand ToggleCursorFollowsMouseComand - => this.toggleCursorFollowsMouseComand ??= new RelayCommand( + public RelayCommand ToggleCursorFollowsMouseCommand + => this.toggleCursorFollowsMouseCommand ??= new RelayCommand( () => this.VisualizationContainer.Navigator.CursorFollowsMouse = !this.VisualizationContainer.Navigator.CursorFollowsMouse); /// @@ -232,7 +242,7 @@ public RelayCommand ToggleCursorFollowsMouseComand [IgnoreDataMember] public RelayCommand NudgeRightCommand => this.nudgeRightCommand ??= new RelayCommand( - () => this.MoveCursorBy(this.nudgeTimeSpan, NearestMessageType.Next), + () => this.MoveCursorBy(this.nudgeTimeSpan, NearestType.Next), () => VisualizationContext.Instance.IsDatasetLoaded() && this.VisualizationContainer.Navigator.CursorMode != CursorMode.Live); /// @@ -242,7 +252,7 @@ public RelayCommand NudgeRightCommand [IgnoreDataMember] public RelayCommand NudgeLeftCommand => this.nudgeLeftCommand ??= new RelayCommand( - () => this.MoveCursorBy(-this.nudgeTimeSpan, NearestMessageType.Previous), + () => this.MoveCursorBy(-this.nudgeTimeSpan, NearestType.Previous), () => VisualizationContext.Instance.IsDatasetLoaded() && this.VisualizationContainer.Navigator.CursorMode != CursorMode.Live); /// @@ -252,7 +262,7 @@ public RelayCommand NudgeLeftCommand [IgnoreDataMember] public RelayCommand JumpRightCommand => this.jumpRightCommand ??= new RelayCommand( - () => this.MoveCursorBy(this.jumpTimeSpan, NearestMessageType.Next), + () => this.MoveCursorBy(this.jumpTimeSpan, NearestType.Next), () => VisualizationContext.Instance.IsDatasetLoaded() && this.VisualizationContainer.Navigator.CursorMode != CursorMode.Live); /// @@ -262,7 +272,7 @@ public RelayCommand JumpRightCommand [IgnoreDataMember] public RelayCommand JumpLeftCommand => this.jumpLeftCommand ??= new RelayCommand( - () => this.MoveCursorBy(-this.jumpTimeSpan, NearestMessageType.Previous), + () => this.MoveCursorBy(-this.jumpTimeSpan, NearestType.Previous), () => VisualizationContext.Instance.IsDatasetLoaded() && this.VisualizationContainer.Navigator.CursorMode != CursorMode.Live); /// @@ -285,13 +295,8 @@ public RelayCommand OpenStoreCommand if (result == true) { string filename = openFileDialog.FileName; - await VisualizationContext.Instance.OpenDatasetAsync(filename, true, this.AppSettings.AutoSaveDatasets); - if (this.AppSettings.AutoLoadMRUDatasetOnStartUp) - { - this.AppSettings.MRUDatasetFilename = filename; - } - - this.EnsureDerivedStreamTreeNodesExist(); + await VisualizationContext.Instance.OpenDatasetAsync(filename, true, this.Settings.AutoSaveDatasets); + this.Settings.AddRecentlyUsedDatasetFilename(filename); } }); @@ -314,16 +319,24 @@ public RelayCommand OpenDatasetCommand if (result == true) { string filename = openFileDialog.FileName; - await VisualizationContext.Instance.OpenDatasetAsync(filename, true, this.AppSettings.AutoSaveDatasets); - this.EnsureDerivedStreamTreeNodesExist(); - - if (this.AppSettings.AutoLoadMRUDatasetOnStartUp) - { - this.AppSettings.MRUDatasetFilename = filename; - } + await VisualizationContext.Instance.OpenDatasetAsync(filename, true, this.Settings.AutoSaveDatasets); + this.Settings.AddRecentlyUsedDatasetFilename(filename); } }); + /// + /// Gets the open recently used dataset command. + /// + [Browsable(false)] + [IgnoreDataMember] + public RelayCommand OpenRecentlyUsedDatasetCommand + => this.openRecentlyUsedDatasetCommand ??= new RelayCommand( + async (filename) => + { + await VisualizationContext.Instance.OpenDatasetAsync(filename, true, this.Settings.AutoSaveDatasets); + this.Settings.AddRecentlyUsedDatasetFilename(filename); + }); + /// /// Gets the save dataset command. /// @@ -346,11 +359,7 @@ public RelayCommand SaveDatasetAsCommand // this should be a relatively quick operation so no need to show progress await VisualizationContext.Instance.DatasetViewModel.SaveAsAsync(filename); - - if (this.AppSettings.AutoLoadMRUDatasetOnStartUp) - { - this.AppSettings.MRUDatasetFilename = filename; - } + this.Settings.AddRecentlyUsedDatasetFilename(filename); } }); @@ -651,8 +660,8 @@ public RelayCommand ClosedCommand => this.closedCommand ??= new RelayCommand( () => { - // Ensure playback is stopped before exiting - this.VisualizationContainer.Navigator.SetCursorMode(CursorMode.Manual); + // Explicitly dispose the VisualizationContext to clean up resources before closing + VisualizationContext.Instance?.Dispose(); // Explicitly dispose so that DataManager doesn't keep the app running for a while longer. DataManager.Instance?.Dispose(); @@ -674,23 +683,23 @@ public RelayCommand ExitCommand public RelayCommand CreateAnnotationStreamCommand => this.createAnnotationStreamCommand ??= new RelayCommand(() => this.CreateAnnotationStream()); - /*/// - /// Gets the show settings window command. + /// + /// Gets the edit settings command. /// [Browsable(false)] [IgnoreDataMember] - public RelayCommand ShowSettingsWindowComand - { - get - { - if (this.showSettingsWindowComand == null) - { - this.showSettingsWindowComand = new RelayCommand(() => this.ShowSettingsWindow()); - } + public RelayCommand EditSettingsCommand + => this.editSettingsCommand ??= new RelayCommand(() => this.EditSettings()); - return this.showSettingsWindowComand; - } - }*/ + /// + /// Gets the command for viewing the error log for additional assembly load. + /// + [Browsable(false)] + [IgnoreDataMember] + public RelayCommand ViewAdditionalAssemblyLoadErrorLogCommand + => this.viewAdditionalAssemblyLoadErrorLogCommand ??= new RelayCommand( + () => this.ViewAdditionalAssemblyLoadErrorLog(), + () => File.Exists(Path.Combine(PsiStudioDocumentsPath, "VisualizersLog.txt"))); /// /// Gets or sets the collection of available layouts. @@ -725,11 +734,11 @@ public LayoutInfo CurrentLayout this.currentLayout = value; if (this.currentLayout == null || this.currentLayout.Name == this.newLayoutName) { - this.AppSettings.CurrentLayoutName = null; + this.Settings.MostRecentlyUsedLayoutName = null; } else { - this.AppSettings.CurrentLayoutName = this.currentLayout.Name; + this.Settings.MostRecentlyUsedLayoutName = this.currentLayout.Name; } if (this.currentLayout != null && this.isInitialized) @@ -761,12 +770,12 @@ public bool OnClosing() } // Put the current state of the timing buttons into the settings object - this.AppSettings.ShowAbsoluteTiming = this.VisualizationContainer.Navigator.ShowAbsoluteTiming; - this.AppSettings.ShowTimingRelativeToSessionStart = this.VisualizationContainer.Navigator.ShowTimingRelativeToSessionStart; - this.AppSettings.ShowTimingRelativeToSelectionStart = this.VisualizationContainer.Navigator.ShowTimingRelativeToSelectionStart; + this.Settings.ShowAbsoluteTiming = this.VisualizationContainer.Navigator.ShowAbsoluteTiming; + this.Settings.ShowTimingRelativeToSessionStart = this.VisualizationContainer.Navigator.ShowTimingRelativeToSessionStart; + this.Settings.ShowTimingRelativeToSelectionStart = this.VisualizationContainer.Navigator.ShowTimingRelativeToSelectionStart; // Save the settings - this.AppSettings.Save(); + this.Settings.Save(); return true; } @@ -783,7 +792,7 @@ public async void CreateAnnotationStream() return; } - var createAnnotationStreamWindow = new CreateAnnotationStreamWindow(currentSession.PartitionViewModels, this.annotationSchemas, Application.Current.MainWindow); + var createAnnotationStreamWindow = new CreateAnnotationStreamWindow(currentSession, this.annotationSchemas, Application.Current.MainWindow); if (createAnnotationStreamWindow.ShowDialog() == true) { var annotationSchema = createAnnotationStreamWindow.SelectedAnnotationSchema; @@ -863,14 +872,14 @@ public async void CreateAnnotationStream() } } - private void MoveCursorBy(TimeSpan timeSpan, NearestMessageType nearestMessageType) + private void MoveCursorBy(TimeSpan timeSpan, NearestType nearestType) { var visContainer = this.VisualizationContainer; var nav = visContainer.Navigator; var time = nav.Cursor + timeSpan; if (visContainer.SnapToVisualizationObject is IStreamVisualizationObject vo) { - nav.MoveCursorTo(DataManager.Instance.GetTimeOfNearestMessage(vo.StreamSource, time, nearestMessageType) ?? time); + nav.MoveCursorTo(DataManager.Instance.GetTimeOfNearestMessage(vo.StreamSource, time, nearestType) ?? time); } else { @@ -887,13 +896,10 @@ private void OpenCurrentLayout() } else { - // Attempt to open the current layout - bool success = VisualizationContext.Instance.OpenLayout(this.CurrentLayout.Path, this.CurrentLayout.Name); - if (success) - { - this.EnsureDerivedStreamTreeNodesExist(); - } - else + // Attempt to open the current layout. User consent may be needed for layouts containing scripts. + this.userConsentObtained.TryGetValue(this.CurrentLayout.Name, out bool userConsent); + bool success = VisualizationContext.Instance.OpenLayout(this.CurrentLayout.Path, this.CurrentLayout.Name, ref userConsent); + if (!success) { // If the load failed, load the default layout instead. This method // may have been initially called by the SelectedItemChanged handler @@ -902,6 +908,10 @@ private void OpenCurrentLayout() // back rather than set it directly here. Application.Current?.Dispatcher.InvokeAsync(() => this.CurrentLayout = this.AvailableLayouts[0]); } + else + { + this.userConsentObtained[this.CurrentLayout.Name] = userConsent; + } } } @@ -925,39 +935,16 @@ private async void OnMainWindowContentRendered(object sender, EventArgs e) string[] args = Environment.GetCommandLineArgs(); if (args.Length > 1) { - await VisualizationContext.Instance.OpenDatasetAsync(args[1], true, this.AppSettings.AutoSaveDatasets); - this.EnsureDerivedStreamTreeNodesExist(); + await VisualizationContext.Instance.OpenDatasetAsync(args[1], true, this.Settings.AutoSaveDatasets); } - else if (this.AppSettings.AutoLoadMRUDatasetOnStartUp && this.AppSettings.MRUDatasetFilename != null) + else if (this.Settings.AutoLoadMostRecentlyUsedDatasetOnStartUp && + this.Settings.MostRecentlyUsedDatasetFilenames != null && + this.Settings.MostRecentlyUsedDatasetFilenames.Any()) { - await VisualizationContext.Instance.OpenDatasetAsync(this.AppSettings.MRUDatasetFilename, true, this.AppSettings.AutoSaveDatasets); - this.EnsureDerivedStreamTreeNodesExist(); - } - } - } - - private void EnsureDerivedStreamTreeNodesExist() - { - if (VisualizationContext.Instance.DatasetViewModel != null) - { - // Check if the visualization container contains any stream member visualizers. - var derivedStreamVisualizationObjects = this.VisualizationContainer.GetDerivedStreamVisualizationObjects(); - - // Get the current session - var currentSessionViewModel = VisualizationContext.Instance.DatasetViewModel.CurrentSessionViewModel; - - if (currentSessionViewModel != null) - { - foreach (IStreamVisualizationObject derivedStreamVisualizationObject in derivedStreamVisualizationObjects) - { - // Get the stream tree node for stream being used by the stream member visualizer. - var streamTreeNode = currentSessionViewModel.FindStreamTreeNode( - derivedStreamVisualizationObject.StreamBinding.PartitionName, - derivedStreamVisualizationObject.StreamBinding.StreamName); - - // If the session contains the stream, ensure its member children have been created. - streamTreeNode?.EnsureDerivedStreamExists(derivedStreamVisualizationObject.StreamBinding); - } + await VisualizationContext.Instance.OpenDatasetAsync( + this.Settings.MostRecentlyUsedDatasetFilenames.First(), + true, + this.Settings.AutoSaveDatasets); } } } @@ -985,18 +972,22 @@ private void InitializeVisualizerMap() // If we have any additional assemblies to search for visualization // classes, display the security warning before proceeding. - if ((this.AppSettings.AdditionalAssembliesList != null) && (this.AppSettings.AdditionalAssembliesList.Count > 0)) + if ((this.Settings.AdditionalAssemblies != null) && (this.Settings.AdditionalAssemblies.Count > 0)) { - var additionalAssembliesWindow = new AdditionalAssembliesWindow(Application.Current.MainWindow, this.AppSettings.AdditionalAssembliesList); - - if (additionalAssembliesWindow.ShowDialog() == true) + if (!this.Settings.ShowSecurityWarningOnLoadingThirdPartyCode || + new AdditionalAssembliesWindow(Application.Current.MainWindow, this.Settings.AdditionalAssemblies).ShowDialog() == true) { - additionalAssemblies.AddRange(this.AppSettings.AdditionalAssembliesList); + additionalAssemblies.AddRange(this.Settings.AdditionalAssemblies); } } // Initialize the visualizer map - VisualizationContext.Instance.PluginMap.Initialize(additionalAssemblies, Path.Combine(PsiStudioDocumentsPath, "VisualizersLog.txt")); + VisualizationContext.Instance.PluginMap.Initialize( + additionalAssemblies, + this.Settings.TypeMappings, + Path.Combine(PsiStudioDocumentsPath, "VisualizersLog.txt"), + this.Settings.ShowErrorLogOnLoadingAdditionalAssemblies, + PsiStudioBatchProcessingTaskConfigurationsPath); } private void UpdateLayoutList() @@ -1026,9 +1017,9 @@ private void UpdateLayoutList() this.AvailableLayouts = layouts; this.RaisePropertyChanged(nameof(this.AvailableLayouts)); - // Set the current layout if it's in the available layouts, otherwise make "new layout" the current layout - LayoutInfo lastLayout = this.AvailableLayouts.FirstOrDefault(l => l.Name == this.AppSettings.CurrentLayoutName); - this.CurrentLayout = lastLayout ?? this.AvailableLayouts[0]; + // Set the most recently used layout if it's in the available layouts, otherwise make "new layout" the current layout + var mostRecentlyUsedLayout = this.AvailableLayouts.FirstOrDefault(l => l.Name == this.Settings.MostRecentlyUsedLayoutName); + this.CurrentLayout = mostRecentlyUsedLayout ?? this.AvailableLayouts[0]; } private void SaveLayoutAs() @@ -1143,11 +1134,11 @@ private void ExpandOrCollapseDatasetsTreeView(bool expand) { if (expand) { - partitionViewModel.StreamTreeRoot.ExpandAll(); + partitionViewModel.RootStreamTreeNode.ExpandAll(); } else { - partitionViewModel.StreamTreeRoot.CollapseAll(); + partitionViewModel.RootStreamTreeNode.CollapseAll(); } partitionViewModel.IsTreeNodeExpanded = expand; @@ -1195,7 +1186,7 @@ private void SynchronizeDatasetsTreeToVisualizationsTree() var partitionViewModel = VisualizationContext.Instance.DatasetViewModel.CurrentSessionViewModel.PartitionViewModels.FirstOrDefault(p => p.Name == streamBinding.PartitionName); if (partitionViewModel != null) { - if (partitionViewModel.SelectNode(streamBinding.NodePath)) + if (partitionViewModel.SelectStreamTreeNode(streamBinding.StreamName)) { VisualizationContext.Instance.DatasetViewModel.CurrentSessionViewModel.IsTreeNodeExpanded = true; VisualizationContext.Instance.DatasetViewModel.IsTreeNodeExpanded = true; @@ -1213,7 +1204,7 @@ private void OnVisualizationContextPropertyChanging(object sender, PropertyChang // Unhook property changed events from old visualization container if (VisualizationContext.Instance.VisualizationContainer != null) { - VisualizationContext.Instance.VisualizationContainer.PropertyChanged -= this.VisualizationContainer_PropertyChanged; + VisualizationContext.Instance.VisualizationContainer.PropertyChanged -= this.OnVisualizationContainerPropertyChanged; } this.RaisePropertyChanging(nameof(this.VisualizationContainer)); @@ -1223,7 +1214,7 @@ private void OnVisualizationContextPropertyChanging(object sender, PropertyChang // Unhook property changed events from old dataset view model if (VisualizationContext.Instance.DatasetViewModel != null) { - VisualizationContext.Instance.DatasetViewModel.PropertyChanged -= this.DatasetViewModel_PropertyChanged; + VisualizationContext.Instance.DatasetViewModel.PropertyChanged -= this.OnDatasetViewModelPropertyChanged; } this.RaisePropertyChanged(nameof(this.TitleText)); @@ -1237,7 +1228,7 @@ private void OnVisualizationContextPropertyChanged(object sender, PropertyChange // Hook property changed events to new visualization container if (VisualizationContext.Instance.VisualizationContainer != null) { - VisualizationContext.Instance.VisualizationContainer.PropertyChanged += this.VisualizationContainer_PropertyChanged; + VisualizationContext.Instance.VisualizationContainer.PropertyChanged += this.OnVisualizationContainerPropertyChanged; // Update the window title to reflect any change in the snap-to stream this.RaisePropertyChanged(nameof(this.TitleText)); @@ -1250,7 +1241,7 @@ private void OnVisualizationContextPropertyChanged(object sender, PropertyChange // Hook property changed events to new dataset view model if (VisualizationContext.Instance.DatasetViewModel != null) { - VisualizationContext.Instance.DatasetViewModel.PropertyChanged += this.DatasetViewModel_PropertyChanged; + VisualizationContext.Instance.DatasetViewModel.PropertyChanged += this.OnDatasetViewModelPropertyChanged; } // Update the window title to reflect the new dataset @@ -1258,7 +1249,23 @@ private void OnVisualizationContextPropertyChanged(object sender, PropertyChange } } - private void DatasetViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e) + private void OnNavigatorPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(Navigator.ShowAbsoluteTiming)) + { + this.Settings.ShowAbsoluteTiming = this.VisualizationContainer.Navigator.ShowAbsoluteTiming; + } + else if (e.PropertyName == nameof(Navigator.ShowTimingRelativeToSelectionStart)) + { + this.Settings.ShowTimingRelativeToSelectionStart = this.VisualizationContainer.Navigator.ShowTimingRelativeToSelectionStart; + } + else if (e.PropertyName == nameof(Navigator.ShowTimingRelativeToSessionStart)) + { + this.Settings.ShowTimingRelativeToSessionStart = this.VisualizationContainer.Navigator.ShowTimingRelativeToSessionStart; + } + } + + private void OnDatasetViewModelPropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(DatasetViewModel.Name)) { @@ -1266,7 +1273,7 @@ private void DatasetViewModel_PropertyChanged(object sender, PropertyChangedEven } } - private void VisualizationContainer_PropertyChanged(object sender, PropertyChangedEventArgs e) + private void OnVisualizationContainerPropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(VisualizationContext.Instance.VisualizationContainer.SnapToVisualizationObject)) { @@ -1274,22 +1281,41 @@ private void VisualizationContainer_PropertyChanged(object sender, PropertyChang } } - /*/// - /// Display the settings dialog. - /// - private void ShowSettingsWindow() + private void EditSettings() { - SettingsWindow dlg = new SettingsWindow(); - dlg.Owner = App.Current.MainWindow; - dlg.LayoutsDirectory = this.AppSettings.LayoutsDirectory; - if (dlg.ShowDialog() == true) + var psiStudioSettingsWindow = new PsiStudioSettingsWindow(Application.Current.MainWindow) { - this.AppSettings.LayoutsDirectory = dlg.LayoutsDirectory; - this.UpdateLayoutList(); + SettingsViewModel = new PsiStudioSettingsViewModel(this.Settings), + }; - // Make "new layout" the current layout - this.CurrentLayout = this.AvailableLayouts[0]; + if (psiStudioSettingsWindow.ShowDialog() == true) + { + bool requiresRestart = psiStudioSettingsWindow.SettingsViewModel.UpdateSettings(this.Settings); + this.VisualizationContainer.Navigator.ShowAbsoluteTiming = this.Settings.ShowAbsoluteTiming; + this.VisualizationContainer.Navigator.ShowTimingRelativeToSelectionStart = this.Settings.ShowTimingRelativeToSelectionStart; + this.VisualizationContainer.Navigator.ShowTimingRelativeToSessionStart = this.Settings.ShowTimingRelativeToSessionStart; + this.Settings.Save(); + + if (requiresRestart) + { + new MessageBoxWindow( + Application.Current.MainWindow, + "Information", + "Some of the changes you have made to the settings will only take effect on the next start of Platform for Situated Intelligence Studio.", + "OK", + null) + .ShowDialog(); + } } - }*/ + } + + private void ViewAdditionalAssemblyLoadErrorLog() + { + string logFilePath = Path.Combine(PsiStudioDocumentsPath, "VisualizersLog.txt"); + if (File.Exists(logFilePath)) + { + Process.Start("notepad.exe", logFilePath); + } + } } } diff --git a/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/Microsoft.Psi.PsiStudio.csproj b/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/Microsoft.Psi.PsiStudio.csproj index 498fb35be..82e5aa62a 100644 --- a/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/Microsoft.Psi.PsiStudio.csproj +++ b/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/Microsoft.Psi.PsiStudio.csproj @@ -28,7 +28,6 @@ - @@ -38,9 +37,9 @@ + - diff --git a/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/PsiStudioSettings.cs b/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/PsiStudioSettings.cs index 2c6f6efc4..48507bd7b 100644 --- a/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/PsiStudioSettings.cs +++ b/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/PsiStudioSettings.cs @@ -6,6 +6,7 @@ namespace Microsoft.Psi.PsiStudio using System; using System.Collections.Generic; using System.IO; + using System.Linq; using System.Windows; using System.Xml; using System.Xml.Serialization; @@ -16,141 +17,199 @@ namespace Microsoft.Psi.PsiStudio /// public class PsiStudioSettings { + /// + /// Current version of serialized settings file. + /// + /// + /// It is not necessary to bump the version when adding or removing properties unless the serialized + /// schema for an existing property has changed in a way which necessitates special handling. + /// + public const int CurrentVersion = 2; + private string settingsFilename; /// - /// Initializes a new instance of the class. + /// Gets or sets the version of the settings object for serialization. /// - public PsiStudioSettings() - { - // Set defaults for all settings - this.WindowPositionLeft = "100"; - this.WindowPositionTop = "100"; - this.WindowWidth = "1024"; - this.WindowHeight = "768"; - this.WindowState = "Normal"; - this.TreeViewPanelWidth = "300"; - this.PropertiesPanelWidth = "300"; - this.DatasetsTabHeight = "400"; - this.ShowAbsoluteTiming = false; - this.ShowTimingRelativeToSessionStart = false; - this.ShowTimingRelativeToSelectionStart = false; - this.CurrentLayoutName = null; - this.AutoSaveDatasets = false; - this.AutoLoadMRUDatasetOnStartUp = false; - this.MRUDatasetFilename = null; - this.AdditionalAssemblies = null; - } + /// + /// This defaults to 0 unless explicitly set in order to distinguish between older versions + /// of the settings file (which did not have the Version attribute), and newer versions which + /// which will contain the Version attribute set to CurrentVersion. Note that it is not necessary + /// to bump CurrentVersion every time changes are made to PsiStudioSettings unless the serialized + /// schema of an existing property has changed between versions and special handling is required + /// for it. Simply adding or removing properties should not require a version change provided + /// default values are provided for any added properties. + /// + [XmlAttribute(AttributeName = "Version")] + public int Version { get; set; } /// /// Gets or sets the main window left position. /// - public string WindowPositionLeft { get; set; } + public string WindowPositionLeft { get; set; } = "100"; /// /// Gets or sets the main window left position. /// - public string WindowPositionTop { get; set; } + public string WindowPositionTop { get; set; } = "100"; /// /// Gets or sets the main window left position. /// - public string WindowWidth { get; set; } + public string WindowWidth { get; set; } = "1024"; /// /// Gets or sets the main window left position. /// - public string WindowHeight { get; set; } + public string WindowHeight { get; set; } = "768"; /// /// Gets or sets the window state (normal, minimized, maximized). /// - public string WindowState { get; set; } + public string WindowState { get; set; } = "Normal"; /// /// Gets or sets the width of the tree views panel. /// - public string TreeViewPanelWidth { get; set; } + public string TreeViewPanelWidth { get; set; } = "300"; /// /// Gets or sets the width of the properties panel. /// - public string PropertiesPanelWidth { get; set; } + public string PropertiesPanelWidth { get; set; } = "300"; /// /// Gets or sets the height of the datasets tab. /// - public string DatasetsTabHeight { get; set; } + public string DatasetsTabHeight { get; set; } = "400"; /// - /// Gets or sets the current layout. + /// Gets or sets the name of the most recently used layout. /// - public string CurrentLayoutName { get; set; } + public string MostRecentlyUsedLayoutName { get; set; } = null; /// /// Gets or sets a value indicating whether the show absolute timing button should be pressed. /// - public bool ShowAbsoluteTiming { get; set; } + public bool ShowAbsoluteTiming { get; set; } = false; /// /// Gets or sets a value indicating whether the show timing relative to session start button should be pressed. /// - public bool ShowTimingRelativeToSessionStart { get; set; } + public bool ShowTimingRelativeToSessionStart { get; set; } = false; /// /// Gets or sets a value indicating whether the show timing relative to selection start button should be pressed. /// - public bool ShowTimingRelativeToSelectionStart { get; set; } + public bool ShowTimingRelativeToSelectionStart { get; set; } = false; /// /// Gets or sets a value indicating whether to set any open dataset object into autosave mode. /// - public bool AutoSaveDatasets { get; set; } + public bool AutoSaveDatasets { get; set; } = false; /// /// Gets or sets a value indicating whether to automatically load the most recently used dataset upon startup. /// - public bool AutoLoadMRUDatasetOnStartUp { get; set; } + public bool AutoLoadMostRecentlyUsedDatasetOnStartUp { get; set; } = false; /// - /// Gets or sets a value indicating the most recently used dataset filename. + /// Gets or sets the list of most recently used dataset filenames. /// - public string MRUDatasetFilename { get; set; } + [XmlArray("MostRecentlyUsedDatasetFilenames")] + [XmlArrayItem("Filename")] + public List MostRecentlyUsedDatasetFilenames { get; set; } = new (); /// /// Gets or sets the list of add-in assemblies. /// - public string AdditionalAssemblies { get; set; } + [XmlArray("AdditionalAssemblies")] + [XmlArrayItem("Assembly")] + public List AdditionalAssemblies { get; set; } = new (); /// - /// Gets the list of add-in components. + /// Gets or sets the set of type mappings. /// [XmlIgnore] - public List AdditionalAssembliesList { get; private set; } + public Dictionary TypeMappings + { + get + { + var typeMappings = new Dictionary(); + foreach (string typeMappingString in this.TypeMappingsAsStringList) + { + var typeMappingKeyValue = typeMappingString.Split(':'); + + // Skip incorrect mapping strings which are not in the form "OldType:NewType" + if (typeMappingKeyValue.Length == 2) + { + typeMappings[typeMappingKeyValue[0].Trim()] = typeMappingKeyValue[1].Trim(); + } + } + + return typeMappings; + } + + set + { + this.TypeMappingsAsStringList = value.Select(t => t.Key + ':' + t.Value).ToList(); + } + } + + /// + /// Gets or sets the string representation of the type mappings. + /// + [XmlArray("TypeMappings")] + [XmlArrayItem("TypeMapping")] + public List TypeMappingsAsStringList { get; set; } = new (); + + /// + /// Gets or sets a value indicating whether to show the security warning when loading + /// third party assemblies or code. + /// + public bool ShowSecurityWarningOnLoadingThirdPartyCode { get; set; } = true; + + /// + /// Gets or sets a value indicating whether to show the error log when loading + /// third party assemblies. + /// + public bool ShowErrorLogOnLoadingAdditionalAssemblies { get; set; } = true; /// /// Loads the settings from file. /// - /// The full name and path of the settings file. + /// The full name and path of the settings file. /// The psi studio settings object that was loaded. - public static PsiStudioSettings Load(string settingsFilename) + public static PsiStudioSettings Load(string psiStudioSettingsFilename) { // Create the settings instance var settings = new PsiStudioSettings(); // Update the settings with those from the file on disk - settings.LoadFromFile(settingsFilename); - - // Generate the additional assemblies list - settings.AdditionalAssembliesList = new List(); - if (!string.IsNullOrWhiteSpace(settings.AdditionalAssemblies)) + if (File.Exists(psiStudioSettingsFilename)) { - foreach (string additionalAssembly in settings.AdditionalAssemblies.Split(';')) + try + { + using (var reader = XmlReader.Create(psiStudioSettingsFilename, new XmlReaderSettings() { XmlResolver = null })) + { + var xmlSerializer = new XmlSerializer(typeof(PsiStudioSettings)); + settings = (PsiStudioSettings)xmlSerializer.Deserialize(reader); + } + + // Attempt to read from an older version of the settings file + if (settings.Version < CurrentVersion) + { + settings.LoadFromPreviousVersionSettingsFile(psiStudioSettingsFilename); + } + } + catch (XmlException) { - settings.AdditionalAssembliesList.Add(additionalAssembly.Trim()); } } + // Set the file name and current version + settings.settingsFilename = psiStudioSettingsFilename; + settings.Version = CurrentVersion; return settings; } @@ -194,37 +253,72 @@ public void Save() } } + /// + /// Adds the most recently used dataset filename to the most recently used list. + /// Will promote it to the top if already present in the list. + /// + /// The dataset filename to add to the most recently used list. + public void AddRecentlyUsedDatasetFilename(string filename) + { + int index = this.MostRecentlyUsedDatasetFilenames.IndexOf(filename); + if (index == -1) + { + if (this.MostRecentlyUsedDatasetFilenames.Count() == 10) + { + this.MostRecentlyUsedDatasetFilenames.RemoveAt(9); + } + + this.MostRecentlyUsedDatasetFilenames.Insert(0, filename); + } + else if (index > 0) + { + // Move filename to the top of MRU list + this.MostRecentlyUsedDatasetFilenames.RemoveAt(index); + this.MostRecentlyUsedDatasetFilenames.Insert(0, filename); + } + } + private string CreateTempFilename() { return this.settingsFilename.Substring(0, this.settingsFilename.IndexOf('.')) + ".tmp"; } /// - /// Loads settings from the xml settings file. + /// Loads settings from older version of the xml settings file. /// - private void LoadFromFile(string settingsFilename) + private void LoadFromPreviousVersionSettingsFile(string settingsFilename) { - this.settingsFilename = settingsFilename; - // Load the settings XML file if it exists - if (File.Exists(this.settingsFilename)) + if (File.Exists(settingsFilename)) { try { var settingsDocument = new XmlDocument() { XmlResolver = null }; - var textReader = new System.IO.StreamReader(this.settingsFilename); + var textReader = new StreamReader(settingsFilename); var reader = XmlReader.Create(textReader, new XmlReaderSettings() { XmlResolver = null }); settingsDocument.Load(reader); - // Get the list of settings - foreach (var propertyInfo in typeof(PsiStudioSettings).GetProperties()) + if (this.Version < 2) { - // Check if this setting has a value in the settings file - var node = settingsDocument.DocumentElement.SelectSingleNode(string.Format("/{0}/{1}", typeof(PsiStudioSettings).Name, propertyInfo.Name)); - if (node != null) + var node = settingsDocument.DocumentElement.SelectSingleNode(string.Format("/{0}/{1}", typeof(PsiStudioSettings).Name, "MostRecentlyUsedDatasetFilename")); + + // Settings files prior to version 2 only saved a single MostRecentlyUsedDatasetFilename rather than a list of most recently used datasets + if (node?.FirstChild?.NodeType == XmlNodeType.Text) + { + this.AddRecentlyUsedDatasetFilename(node.InnerText); + } + } + + if (this.Version == 0) + { + var node = settingsDocument.DocumentElement.SelectSingleNode(string.Format("/{0}/{1}", typeof(PsiStudioSettings).Name, nameof(this.AdditionalAssemblies))); + + // Version 0 saved AdditionalAssemblies as a single text element instead of an array + if (node?.FirstChild?.NodeType == XmlNodeType.Text) { - // Update the setting with the value from the settings file - propertyInfo.SetValue(this, Convert.ChangeType(node.InnerText, propertyInfo.PropertyType)); + // AdditionalAssemblies was previously serialized as a semicolon-separated string + this.AdditionalAssemblies.Clear(); + this.AdditionalAssemblies.AddRange(node.InnerText.Split(';')); } } } diff --git a/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/PsiStudioSettingsViewModel.cs b/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/PsiStudioSettingsViewModel.cs new file mode 100644 index 000000000..8c6863d63 --- /dev/null +++ b/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/PsiStudioSettingsViewModel.cs @@ -0,0 +1,371 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.Psi.PsiStudio +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.ComponentModel; + using System.Linq; + using System.Text; + using Microsoft.Psi.Data; + using Xceed.Wpf.Toolkit.PropertyGrid.Attributes; + + /// + /// Implements a view model for Psi Studio settings. + /// + public class PsiStudioSettingsViewModel : ObservableObject, INotifyDataErrorInfo + { + private readonly Dictionary> validationErrors = new (); + private List typeMappingsAsStringList; + + /// + /// Initializes a new instance of the class. + /// + /// The current psi studio settings. + public PsiStudioSettingsViewModel(PsiStudioSettings psiStudioSettings) + { + // Set defaults for all settings + this.WindowPositionLeft = psiStudioSettings.WindowPositionLeft; + this.WindowPositionTop = psiStudioSettings.WindowPositionTop; + this.WindowWidth = psiStudioSettings.WindowWidth; + this.WindowHeight = psiStudioSettings.WindowHeight; + this.WindowState = psiStudioSettings.WindowState; + this.TreeViewPanelWidth = psiStudioSettings.TreeViewPanelWidth; + this.PropertiesPanelWidth = psiStudioSettings.PropertiesPanelWidth; + this.DatasetsTabHeight = psiStudioSettings.DatasetsTabHeight; + this.ShowAbsoluteTiming = psiStudioSettings.ShowAbsoluteTiming; + this.ShowTimingRelativeToSessionStart = psiStudioSettings.ShowTimingRelativeToSessionStart; + this.ShowTimingRelativeToSelectionStart = psiStudioSettings.ShowTimingRelativeToSelectionStart; + this.MostRecentlyUsedLayoutName = psiStudioSettings.MostRecentlyUsedLayoutName; + this.AutoSaveDatasets = psiStudioSettings.AutoSaveDatasets; + this.AutoLoadMostRecentlyUsedDatasetOnStartUp = psiStudioSettings.AutoLoadMostRecentlyUsedDatasetOnStartUp; + this.MostRecentlyUsedDatasetFilenames = psiStudioSettings.MostRecentlyUsedDatasetFilenames; + + // Generate a copy of the additional assemblies list + this.AdditionalAssembliesAsStringList = psiStudioSettings.AdditionalAssemblies.ToList(); + + // Generate a copy of the type mappings + this.TypeMappingsAsStringList = psiStudioSettings.TypeMappingsAsStringList.ToList(); + + this.ShowSecurityWarningOnLoadingThirdPartyCode = psiStudioSettings.ShowSecurityWarningOnLoadingThirdPartyCode; + this.ShowErrorLogOnLoadingAdditionalAssemblies = psiStudioSettings.ShowErrorLogOnLoadingAdditionalAssemblies; + } + + /// + public event EventHandler ErrorsChanged; + + /// + /// Gets or sets the main window left position. + /// + [Browsable(false)] + public string WindowPositionLeft { get; set; } + + /// + /// Gets or sets the main window top position. + /// + [Browsable(false)] + public string WindowPositionTop { get; set; } + + /// + /// Gets or sets the main window width. + /// + [Browsable(false)] + public string WindowWidth { get; set; } + + /// + /// Gets or sets the main window height. + /// + [Browsable(false)] + public string WindowHeight { get; set; } + + /// + /// Gets or sets the window state (normal, minimized, maximized). + /// + [Browsable(false)] + public string WindowState { get; set; } + + /// + /// Gets or sets the width of the tree views panel. + /// + [Browsable(false)] + public string TreeViewPanelWidth { get; set; } + + /// + /// Gets or sets the width of the properties panel. + /// + [Browsable(false)] + public string PropertiesPanelWidth { get; set; } + + /// + /// Gets or sets the height of the datasets tab. + /// + [Browsable(false)] + public string DatasetsTabHeight { get; set; } + + /// + /// Gets or sets the list of additional assemblies to load. + /// + [PropertyOrder(0)] + [DisplayName("Additional Assemblies")] + [Description("The list of additional third party assemblies to load at start-up.")] + public List AdditionalAssembliesAsStringList { get; set; } + + /// + /// Gets or sets the string representation of the type mappings. + /// + [PropertyOrder(1)] + [DisplayName("Type Mappings")] + [Description("The list of type mappings to use when deserializing streams, specified as a collection of OldType:NewType.")] + public List TypeMappingsAsStringList + { + get => this.typeMappingsAsStringList; + set + { + this.typeMappingsAsStringList = value; + this.ValidateTypeMappingsAsStringList(); + } + } + + /// + /// Gets or sets the name of the most recently used layout. + /// + [PropertyOrder(2)] + [DisplayName("Most Recently Used Layout")] + [Description("The name of the most recently used layout.")] + public string MostRecentlyUsedLayoutName { get; set; } + + /// + /// Gets or sets the list of most recently used dataset filenames. + /// + [PropertyOrder(3)] + [DisplayName("Most Recently Used Dataset Filenames")] + [Description("The list of most recently used dataset filenames.")] + public List MostRecentlyUsedDatasetFilenames { get; set; } + + /// + /// Gets or sets a value indicating whether to set any open dataset object into autosave mode. + /// + [PropertyOrder(4)] + [DisplayName("Auto Save Datasets")] + [Description("Indicates whether datasets should be automatically saved when partitions/sessions are added or removed.")] + public bool AutoSaveDatasets { get; set; } + + /// + /// Gets or sets a value indicating whether to automatically load the most recently used dataset upon startup. + /// + [PropertyOrder(5)] + [DisplayName("Auto Load Most Recently Used Dataset")] + [Description("Indicates whether the most recently used dataset should be automatically loaded at start-up.")] + public bool AutoLoadMostRecentlyUsedDatasetOnStartUp { get; set; } + + /// + /// Gets or sets a value indicating whether the show absolute timing button should be pressed. + /// + [PropertyOrder(6)] + [DisplayName("Show Absolute Time")] + [Description("Indicates whether the navigator should automatically show the Absolute Time clock.")] + public bool ShowAbsoluteTiming { get; set; } + + /// + /// Gets or sets a value indicating whether the show timing relative to session start button should be pressed. + /// + [PropertyOrder(7)] + [DisplayName("Show Time Relative to Session Start")] + [Description("Indicates whether the navigator should automatically show the Time Relative to Session Start clock.")] + public bool ShowTimingRelativeToSessionStart { get; set; } + + /// + /// Gets or sets a value indicating whether the show timing relative to selection start button should be pressed. + /// + [PropertyOrder(8)] + [DisplayName("Show Time Relative to Selection Start")] + [Description("Indicates whether the navigator should automatically show the Time Relative to Selection Start clock.")] + public bool ShowTimingRelativeToSelectionStart { get; set; } + + /// + /// Gets or sets a value indicating whether to skip the security warning when loading + /// third party assemblies or code. + /// + [PropertyOrder(9)] + [DisplayName("Show Security Warning for Third Party Code")] + [Description("Indicates whether to show the security warning when loading additional third party assemblies.")] + public bool ShowSecurityWarningOnLoadingThirdPartyCode { get; set; } + + /// + /// Gets or sets a value indicating whether to show the error log when errors occur + /// while loading third party assemblies or code. + /// + [PropertyOrder(10)] + [DisplayName("Show Error Log when Loading Additional Assemblies")] + [Description("Indicates whether to show the error log when errors occur while loading third party assemblies or code.")] + public bool ShowErrorLogOnLoadingAdditionalAssemblies { get; set; } + + /// + /// Gets the validation errors for this object. + /// + [Browsable(false)] + public string Error + { + get + { + var errorText = new StringBuilder(); + if (this.validationErrors.Count > 0) + { + errorText.AppendLine("There were errors with some of the settings values. Please correct them before saving."); + errorText.AppendLine(); + + // Concatenate the validation errors for each property + foreach (var errors in this.validationErrors) + { + errorText.AppendLine($"{errors.Key}:"); + foreach (var error in errors.Value) + { + errorText.AppendLine(error); + } + } + } + + return errorText.ToString(); + } + } + + /// + [Browsable(false)] + public bool HasErrors => this.validationErrors.Count > 0; + + /// + public IEnumerable GetErrors(string propertyName) + { + return this.validationErrors.TryGetValue(propertyName, out var errors) ? errors : null; + } + + /// + /// Updates the supplied PsiStudio settings object. + /// + /// The PsiStudio settings object to update. + /// True if the configuration update requires a PsiStudio restart. + public bool UpdateSettings(PsiStudioSettings settings) + { + var requiresRestart = false; + var existingAssemblies = settings.AdditionalAssemblies.ToList(); + if (this.AdditionalAssembliesAsStringList.Any(s => !existingAssemblies.Contains(s))) + { + requiresRestart = true; + } + + var existingTypeMappings = settings.TypeMappingsAsStringList; + if (this.TypeMappingsAsStringList.Any(s => !existingTypeMappings.Contains(s))) + { + requiresRestart = true; + } + + settings.WindowPositionLeft = this.WindowPositionLeft; + settings.WindowPositionTop = this.WindowPositionTop; + settings.WindowWidth = this.WindowWidth; + settings.WindowHeight = this.WindowHeight; + settings.WindowState = this.WindowState; + settings.TreeViewPanelWidth = this.TreeViewPanelWidth; + settings.PropertiesPanelWidth = this.PropertiesPanelWidth; + settings.DatasetsTabHeight = this.DatasetsTabHeight; + settings.ShowAbsoluteTiming = this.ShowAbsoluteTiming; + settings.ShowTimingRelativeToSessionStart = this.ShowTimingRelativeToSessionStart; + settings.ShowTimingRelativeToSelectionStart = this.ShowTimingRelativeToSelectionStart; + settings.MostRecentlyUsedLayoutName = this.MostRecentlyUsedLayoutName; + settings.AutoSaveDatasets = this.AutoSaveDatasets; + settings.AutoLoadMostRecentlyUsedDatasetOnStartUp = this.AutoLoadMostRecentlyUsedDatasetOnStartUp; + settings.MostRecentlyUsedDatasetFilenames = this.MostRecentlyUsedDatasetFilenames; + settings.AdditionalAssemblies = this.AdditionalAssembliesAsStringList; + settings.TypeMappingsAsStringList = this.TypeMappingsAsStringList; + settings.ShowSecurityWarningOnLoadingThirdPartyCode = this.ShowSecurityWarningOnLoadingThirdPartyCode; + settings.ShowErrorLogOnLoadingAdditionalAssemblies = this.ShowErrorLogOnLoadingAdditionalAssemblies; + + return requiresRestart; + } + + /// + /// Validator for the TypeMappingsAsStringList property. + /// + private void ValidateTypeMappingsAsStringList() + { + var typeMappings = new Dictionary(); + var syntaxErrors = new List(); + var duplicates = new List(); + + foreach (string mapping in this.TypeMappingsAsStringList) + { + // Ignore whitespace entries + if (!string.IsNullOrWhiteSpace(mapping)) + { + // Ensure that mapping has the correct syntax of OldType:NewType + var types = mapping.Split(':'); + if (types.Length != 2) + { + syntaxErrors.Add(mapping); + } + else + { + // Ensure there are no duplicate mappings for OldType + string fromType = types[0].Trim(); + string toType = types[1].Trim(); + if (typeMappings.ContainsKey(fromType)) + { + duplicates.Add(fromType); + } + else + { + typeMappings.Add(fromType, toType); + } + } + } + } + + // Construct the validation error messages for this property + var errors = new List(); + + if (syntaxErrors.Count > 0) + { + var errorText = new StringBuilder(); + errorText.AppendLine( + "There was an error with some of the type mapping(s). Each mapping should be on " + + "a separate line and be a pair of fully-qualified type names (e.g. OldType:NewType) " + + "separated by a colon. Please correct the following type mapping entries:"); + + syntaxErrors.ForEach(mapping => errorText.AppendLine($"- {mapping}")); + + errors.Add(errorText.ToString()); + } + + if (duplicates.Count > 0) + { + var errorText = new StringBuilder(); + errorText.AppendLine("Duplicate mappings were found for the following types. Please enter only one mapping per type."); + duplicates.ForEach(type => errorText.AppendLine($"- {type}")); + + errors.Add(errorText.ToString()); + } + + // Set the error text if there were validation errors + if (errors.Count > 0) + { + this.validationErrors[nameof(this.TypeMappingsAsStringList)] = errors; + } + else + { + this.validationErrors.Remove(nameof(this.TypeMappingsAsStringList)); + } + + this.RaiseErrorsChanged(nameof(this.TypeMappingsAsStringList)); + } + + /// + /// Raises the event the specified property. + /// + /// The property name. + private void RaiseErrorsChanged(string propertyName) + { + this.ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName)); + } + } +} diff --git a/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/PsiStudioTemplateSelector.cs b/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/PsiStudioTemplateSelector.cs index 1cde0588c..c8b875f15 100644 --- a/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/PsiStudioTemplateSelector.cs +++ b/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/PsiStudioTemplateSelector.cs @@ -30,7 +30,7 @@ public override DataTemplate SelectTemplate(object item, DependencyObject contai { return element.FindResource("PartitionTemplate") as DataTemplate; } - else if (item is StreamContainerTreeNode) + else if (item is StreamTreeNode) { return element.FindResource("StreamTreeNodeTemplate") as DataTemplate; } diff --git a/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/Windows/PsiStudioSettingsWindow.xaml b/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/Windows/PsiStudioSettingsWindow.xaml new file mode 100644 index 000000000..4bbdbfe55 --- /dev/null +++ b/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/Windows/PsiStudioSettingsWindow.xaml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/Windows/PsiStudioSettingsWindow.xaml.cs b/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/Windows/PsiStudioSettingsWindow.xaml.cs new file mode 100644 index 000000000..ee289688d --- /dev/null +++ b/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/Windows/PsiStudioSettingsWindow.xaml.cs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.Psi.Visualization.Windows +{ + using System.Collections; + using System.Windows; + using System.Windows.Controls; + using Microsoft.Psi.PsiStudio; + using Xceed.Wpf.Toolkit.PropertyGrid; + + /// + /// Interaction logic for PsiStudioSettingsWindow.xaml. + /// + public partial class PsiStudioSettingsWindow : Window + { + /// + /// Initializes a new instance of the class. + /// + /// The owner of this window. + public PsiStudioSettingsWindow(Window owner) + { + this.InitializeComponent(); + + this.Owner = owner; + this.DataContext = this; + + // There is an issue with the PropertyGrid where validation errors may not be immediately reflected + // for collection properties (see: https://github.com/xceedsoftware/wpftoolkit/issues/1463). The + // following is a workaround to force evaluation of the validation state for collection properties. + this.PropertyGrid.SelectedObjectChanged += (s, e) => + { + foreach (PropertyItem property in this.PropertyGrid.Properties) + { + // Add a handler for collection type properties to ensure that the property source value is + // updated so that it may be validated, then set the validation status to reflect the result + if (typeof(IEnumerable).IsAssignableFrom(property.PropertyType)) + { + // Triggered when the property editor loses focus + property.LostFocus += (s, e) => + { + var binding = property.GetBindingExpression(PropertyItem.ValueProperty); + if (binding != null && binding.DataItem is DependencyObject source) + { + // Update the bound value to give it an opportunity to validate + binding.UpdateSource(); + + // Update the validation state of the binding + if (Validation.GetHasError(source)) + { + var errors = Validation.GetErrors(source); + Validation.MarkInvalid(binding, errors[0]); + } + else + { + Validation.ClearInvalid(binding); + } + } + }; + } + } + }; + } + + /// + /// Gets or sets the directory to search for layout files. + /// + public PsiStudioSettingsViewModel SettingsViewModel { get; set; } + + private void SaveButtonClick(object sender, RoutedEventArgs e) + { + if (!this.SettingsViewModel.HasErrors) + { + this.DialogResult = true; + this.Close(); + } + else + { + // Display validation errors + new MessageBoxWindow( + this.Owner, + "Settings Error", + this.SettingsViewModel.Error, + cancelButtonText: null).ShowDialog(); + + e.Handled = false; + } + } + } +} \ No newline at end of file diff --git a/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/Windows/SettingsWindow.xaml b/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/Windows/SettingsWindow.xaml deleted file mode 100644 index 8e9f4c291..000000000 --- a/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/Windows/SettingsWindow.xaml +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/Windows/SettingsWindow.xaml.cs b/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/Windows/SettingsWindow.xaml.cs deleted file mode 100644 index d9ab8a8ef..000000000 --- a/Sources/Tools/PsiStudio/Microsoft.Psi.PsiStudio/Windows/SettingsWindow.xaml.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. - -namespace Microsoft.Psi.Visualization.Windows -{ - using System.Windows; - using System.Windows.Forms; - - /// - /// Interaction logic for SettingsWindow.xaml. - /// - public partial class SettingsWindow : Window - { - /// - /// Initializes a new instance of the class. - /// - public SettingsWindow() - { - this.InitializeComponent(); - } - - /// - /// Gets or sets the directory to search for layout files. - /// - public string LayoutsDirectory - { - get { return this.LayoutsDirectoryTextBox.Text; } - set { this.LayoutsDirectoryTextBox.Text = value; } - } - - private void OKButton_Click(object sender, RoutedEventArgs e) - { - this.DialogResult = true; - e.Handled = true; - } - - private void SelectDirectoryButton_Click(object sender, RoutedEventArgs e) - { - FolderBrowserDialog dlg = new FolderBrowserDialog(); - dlg.Description = "Select the folder where PsiStudio will search for layout files (*.plo files)"; - dlg.SelectedPath = this.LayoutsDirectory; - if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK) - { - this.LayoutsDirectory = dlg.SelectedPath; - } - } - } -} diff --git a/Sources/Visualization/Microsoft.Msagl.WpfGraphControl/Microsoft.Msagl.WpfGraphControl.csproj b/Sources/Visualization/Microsoft.Msagl.WpfGraphControl/Microsoft.Msagl.WpfGraphControl.csproj index 84d2f1958..3b5f61845 100644 --- a/Sources/Visualization/Microsoft.Msagl.WpfGraphControl/Microsoft.Msagl.WpfGraphControl.csproj +++ b/Sources/Visualization/Microsoft.Msagl.WpfGraphControl/Microsoft.Msagl.WpfGraphControl.csproj @@ -25,8 +25,8 @@ all runtime; build; native; contentfiles; analyzers - - + + 1.1.3 diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Adapters/ChainedStreamAdapter.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Adapters/ChainedStreamAdapter.cs new file mode 100644 index 000000000..987041efa --- /dev/null +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Adapters/ChainedStreamAdapter.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.Psi.Visualization.Adapters +{ + using System; + using Microsoft.Psi.Visualization.Data; + + /// + /// Implements a stream adapter that chains two different stream adapters. + /// + /// The type of messages in the source stream. + /// The type of the messages produced by the first adapter. + /// The type of the messages produced by the second adapter. + /// The type of the first adapter. + /// The type of the second adapter. + public class ChainedStreamAdapter : StreamAdapter + where TFirstAdapter : StreamAdapter + where TSecondAdapter : StreamAdapter + { + private readonly TFirstAdapter firstAdapter; + private readonly TSecondAdapter secondAdapter; + + /// + /// Initializes a new instance of the class. + /// + /// The parameters for the first adapter. + /// The parameters for the second adapter. + public ChainedStreamAdapter(object[] firstAdapterParameters, object[] secondAdapterParameters) + { + this.firstAdapter = Activator.CreateInstance(typeof(TFirstAdapter), firstAdapterParameters) as TFirstAdapter; + this.secondAdapter = Activator.CreateInstance(typeof(TSecondAdapter), secondAdapterParameters) as TSecondAdapter; + } + + /// + public override TDestination GetAdaptedValue(TSource source, Envelope envelope) + { + var intermediate = this.firstAdapter.GetAdaptedValue(source, envelope); + var result = this.secondAdapter.GetAdaptedValue(intermediate, envelope); + this.firstAdapter.Dispose(intermediate); + return result; + } + + /// + public override void Dispose(TDestination destination) + => this.secondAdapter.Dispose(destination); + } +} diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Adapters/DictionaryKeyToValueAdapter.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Adapters/DictionaryKeyToValueAdapter.cs new file mode 100644 index 000000000..96dadfc6f --- /dev/null +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Adapters/DictionaryKeyToValueAdapter.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.Psi.Visualization.Adapters +{ + using System.Collections.Generic; + using Microsoft.Psi.Visualization.Data; + + /// + /// Implements a stream adapter from a generic dictionary to the value of the specified key string. + /// + /// The type of the generic dictionary key. + /// The type of the generic dictionary values. + /// The type of the destination data. + public class DictionaryKeyToValueAdapter : StreamAdapter, TDestination> + { + private readonly string key; + + /// + /// Initializes a new instance of the class. + /// + /// The key for the adapted value. + public DictionaryKeyToValueAdapter(string key) + : base() + { + this.key = key; + } + + /// + public override TDestination GetAdaptedValue(Dictionary source, Envelope envelope) + { + if (source is not null) + { + foreach (var item in source) + { + if (item.Key.ToString() == this.key) + { + // TDestination may be TValue or Nullable. Cast to object, then to TDestination to handle either case. + return (TDestination)(object)item.Value; + } + } + } + + return default; + } + } +} diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Adapters/InterfaceAdapter.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Adapters/InterfaceAdapter.cs index f8386cc3b..46fcf2e6d 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Adapters/InterfaceAdapter.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Adapters/InterfaceAdapter.cs @@ -10,7 +10,7 @@ namespace Microsoft.Psi.Visualization.Adapters /// Implements a stream adapter from a specified type to an interface. /// /// The source type. - /// The nterface type. + /// The interface type. public class InterfaceAdapter : StreamAdapter where T : TInterface { diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Adapters/ScriptAdapter.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Adapters/ScriptAdapter.cs new file mode 100644 index 000000000..b8c3dadbe --- /dev/null +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Adapters/ScriptAdapter.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.Psi.Visualization.Adapters +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using Microsoft.CodeAnalysis.CSharp.Scripting; + using Microsoft.CodeAnalysis.Scripting; + using Microsoft.Psi.Visualization.Data; + using Microsoft.Psi.Visualization.DataTypes; + + /// + /// Implements a stream adapter that adapts the stream values by running a C# script. + /// + /// The type of the source stream. + /// The type of the result of the script evaluation. + public class ScriptAdapter : StreamAdapter + { + private readonly Task> compileScriptTask; + private Script script; + + /// + /// Initializes a new instance of the class. + /// + /// The C# script to run. + /// The list of usings. + public ScriptAdapter(string scriptCode, IEnumerable usings) + : base() + { + // Run the script compilation task in the background so it will run in the background and be ready upon first use + this.compileScriptTask = Task.Run(() => + { + // Load all currently loaded assemblies, but exclude those that are dynamically generated + var assemblies = AppDomain.CurrentDomain.GetAssemblies().Where(a => !a.IsDynamic && !string.IsNullOrEmpty(a.Location)); + var options = ScriptOptions.Default.WithReferences(assemblies).WithImports(usings); + var script = CSharpScript.Create(scriptCode, options, typeof(ScriptGlobals)); + var diagnostics = script.Compile(); + if (!diagnostics.IsEmpty) + { + throw new CompilationErrorException("Script Error", diagnostics); + } + + // Return the compiled script + return script; + }); + } + + /// + public override TResult GetAdaptedValue(TSource source, Envelope envelope) + { + if (this.script == null) + { + // cache the script once it has finished compiling + this.script = this.compileScriptTask.GetAwaiter().GetResult(); + } + + var globals = new ScriptGlobals(source, envelope); + var result = this.script.RunAsync(globals).Result; + return result.ReturnValue; + } + } +} diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Common/ContextMenuName.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Common/ContextMenuName.cs index aa504fd0e..57700a0c3 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Common/ContextMenuName.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Common/ContextMenuName.cs @@ -79,9 +79,24 @@ internal class ContextMenuName public const string ZoomToStreamExtents = "Zoom to Stream Extents"; /// - /// Expand members. + /// Create a script derived stream. /// - public const string ExpandMembers = "Expand Members"; + public const string AddScriptDerivedStream = "Add Script Derived Stream ..."; + + /// + /// Modify a script for a script derived stream. + /// + public const string ModifyScript = "Modify Script ..."; + + /// + /// Add member derived streams. + /// + public const string AddMemberDerivedStreams = "Add Member Derived Streams"; + + /// + /// Expand dictionary keys. + /// + public const string AddDictionaryKeyDerivedStreams = "Add Dictionary Key Derived Streams"; /// /// Expand all nodes. diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Common/IconSourcePath.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Common/IconSourcePath.cs index 9f7148cb8..a5c37edc1 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Common/IconSourcePath.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Common/IconSourcePath.cs @@ -23,6 +23,11 @@ public static class IconSourcePath /// public const string AnnotationUnbound = IconPrefix + "annotation-unbound.png"; + /// + /// Sets the annotation to the selection boundaries. + /// + public const string SetAnnotationToSelection = IconPrefix + "set-annotation-to-selection.png"; + /// /// Partition. /// @@ -141,27 +146,27 @@ public static class IconSourcePath /// /// A stream member. /// - public const string StreamMember = IconPrefix + "stream-member.png"; + public const string DerivedStream = IconPrefix + "stream-member.png"; /// /// A live stream member. /// - public const string StreamMemberLive = IconPrefix + "stream-member-live.png"; + public const string DerivedStreamLive = IconPrefix + "stream-member-live.png"; /// /// A stream member. /// - public const string StreamMemberSnap = IconPrefix + "stream-member-snap.png"; + public const string DerivedStreamSnap = IconPrefix + "stream-member-snap.png"; /// /// A stream member. /// - public const string StreamMemberSnapLive = IconPrefix + "stream-member-snap-live.png"; + public const string DerivedStreamSnapLive = IconPrefix + "stream-member-snap-live.png"; /// /// A stream member. /// - public const string StreamMemberUnbound = IconPrefix + "stream-member-unbound.png"; + public const string DerivedStreamUnbound = IconPrefix + "stream-member-unbound.png"; /// /// A stream member. diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/ContextMenuItemInfo.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/ContextMenuItemInfo.cs new file mode 100644 index 000000000..bb2e7524b --- /dev/null +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/ContextMenuItemInfo.cs @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.Psi.Visualization +{ + using System.Collections.Generic; + using System.Windows.Input; + + /// + /// Provides information for creating context menu items with command bindings. + /// + public class ContextMenuItemInfo + { + private readonly string displayName; + private readonly ICommand command; + private readonly string iconSourcePath; + private readonly object tag; + private readonly bool isEnabled; + private readonly object commandParameter; + private readonly List subItems; + + /// + /// Initializes a new instance of the class. + /// + /// The path to the icon for this context menu item. + /// The display name of this context menu item. + /// The command to execute. + /// An optional, user-defined tag to associate with the context menu item (default is null). + /// An optional variable indicating whether the context menu item is enabled (default is true). + /// The command parameter, or null if the command does not take a parameter. + public ContextMenuItemInfo(string icon, string displayName, ICommand command, object tag = null, bool isEnabled = true, object commandParameter = null) + { + this.displayName = displayName; + this.command = command; + this.iconSourcePath = icon; + this.commandParameter = commandParameter; + this.isEnabled = isEnabled; + this.tag = tag; + this.subItems = null; + } + + /// + /// Initializes a new instance of the class correspond to a sub-menu. + /// + /// The display name for the submenu. + public ContextMenuItemInfo(string displayName) + { + this.displayName = displayName; + this.command = null; + this.iconSourcePath = null; + this.commandParameter = null; + this.isEnabled = true; + this.tag = null; + this.subItems = new (); + } + + /// + /// Gets the icon source path for the context menu item. + /// + public string IconSourcePath => this.iconSourcePath; + + /// + /// Gets the display name for the context menu item. + /// + public string DisplayName => this.displayName; + + /// + /// Gets the command. + /// + public ICommand Command => this.command; + + /// + /// Gets the command parameter. + /// + public object CommandParameter => this.commandParameter; + + /// + /// Gets a value indicating whether the context menu item is enabled. + /// + public bool IsEnabled => this.isEnabled; + + /// + /// Gets the tag for the context menu item. + /// + public object Tag => this.tag; + + /// + /// Gets the list of subitems. + /// + public List SubItems => this.subItems; + + /// + /// Gets a value indicating whether the context menu item corresponds to a menu with subitems. + /// + public bool HasSubItems => this.subItems != null; + } +} diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Controls/TimelineScroller.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Controls/TimelineScroller.cs index a1dc668a7..f7fcd12f6 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Controls/TimelineScroller.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Controls/TimelineScroller.cs @@ -7,8 +7,8 @@ namespace Microsoft.Psi.Visualization.Controls using System.Windows; using System.Windows.Controls; using System.Windows.Input; + using Microsoft.Psi.Data; using Microsoft.Psi.Visualization.Data; - using Microsoft.Psi.Visualization.Helpers; using Microsoft.Psi.Visualization.Navigation; using Microsoft.Psi.Visualization.VisualizationObjects; @@ -50,7 +50,7 @@ protected override void OnMouseMove(MouseEventArgs e) // find the timestamp of the message that's temporally closest to the mouse pointer. if (VisualizationContext.Instance.VisualizationContainer.SnapToVisualizationObject is IStreamVisualizationObject streamVisualizationObject) { - snappedTime = DataManager.Instance.GetTimeOfNearestMessage(streamVisualizationObject.StreamSource, time, NearestMessageType.Nearest); + snappedTime = DataManager.Instance.GetTimeOfNearestMessage(streamVisualizationObject.StreamSource, time, NearestType.Nearest); } if (snappedTime.HasValue) diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Data/DataManager.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Data/DataManager.cs index 25ae9fb76..101782a31 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Data/DataManager.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Data/DataManager.cs @@ -7,11 +7,10 @@ namespace Microsoft.Psi.Visualization.Data using System.Collections.Generic; using System.Linq; using System.Reflection; - using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Threading; - using Microsoft.Psi.Visualization.Helpers; + using Microsoft.Psi.Data; using Microsoft.Psi.Visualization.Windows; /// @@ -342,12 +341,12 @@ public void SaveStore(string storeName, string storePath, IProgress prog /// /// The stream source specifying the stream of interest. /// The time to find the nearest message to. - /// The type of nearest message to find. + /// The type of nearest message to find. /// The time of the nearest message, if one is found or null otherwise. - public DateTime? GetTimeOfNearestMessage(StreamSource streamSource, DateTime time, NearestMessageType nearestMessageType) + public DateTime? GetTimeOfNearestMessage(StreamSource streamSource, DateTime time, NearestType nearestType) { return streamSource != null ? this.GetOrCreateDataStoreReader(streamSource) - .GetTimeOfNearestMessage(streamSource, time, nearestMessageType) : null; + .GetTimeOfNearestMessage(streamSource, time, nearestType) : null; } /// @@ -378,12 +377,20 @@ private void ReadAndPublishStreamValueTask() } catch (Exception ex) { + string GetMessageTrace(Exception ex, string message) + { + return + ex != null + ? GetMessageTrace(ex.InnerException, message + Environment.NewLine + ex.Message) + : message; + } + Application.Current.Dispatcher.BeginInvoke((Action)(() => { new MessageBoxWindow( Application.Current.MainWindow, "Error", - $"An error occurred while attempting to read stream values.{Environment.NewLine}{Environment.NewLine}{ex.Message}", + GetMessageTrace(ex, $"An error occurred while attempting to read stream values.{Environment.NewLine}"), "Close", null).ShowDialog(); })); diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Data/DataStoreReader.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Data/DataStoreReader.cs index a5e649146..26e7d07c0 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Data/DataStoreReader.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Data/DataStoreReader.cs @@ -12,7 +12,6 @@ namespace Microsoft.Psi.Visualization.Data using System.Threading.Tasks; using Microsoft.Psi; using Microsoft.Psi.Data; - using Microsoft.Psi.Visualization.Helpers; /// /// Implements an object used to read stream data from a specific data store. @@ -28,8 +27,8 @@ public class DataStoreReader : IDisposable /// The list of streams that have been identified as unreadable, probably due to the format of the /// message on disk not matching the current format of the data object they are deserialized from. /// - private readonly List unreadableStreams = new List(); - private readonly List streamDataProviders = new List(); + private readonly List unreadableStreams = new (); + private readonly List streamDataProviders = new (); private IStreamReader streamReader; private List executionContexts; @@ -113,11 +112,11 @@ public void Dispose() /// /// The stream source specifying the stream of interest. /// The time to find the nearest message to. - /// The type of nearest message to find. + /// The type of nearest message to find. /// The time of the nearest message, if one is found or null otherwise. - internal DateTime? GetTimeOfNearestMessage(StreamSource streamSource, DateTime time, NearestMessageType nearestMessageType) => + internal DateTime? GetTimeOfNearestMessage(StreamSource streamSource, DateTime time, NearestType nearestType) => this.GetStreamProviderOrDefault(streamSource.StreamName) - .GetTimeOfNearestMessage(time, nearestMessageType); + .GetTimeOfNearestMessage(time, nearestType); /// /// Gets or creates a stream interval provider for a specified stream source. @@ -271,7 +270,7 @@ internal void ReadAndPublishStreamValues(DateTime dateTime) /// The list of the names of streams that had updates saved. internal string[] SaveChanges(IProgress progress) { - Dictionary> updates = new Dictionary>(); + var updates = new Dictionary>(); // Get all the changes from all the stream readers that have them. foreach (var streamIntervalProvider in this.GetStreamIntervalProviders()) diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Data/IStreamDataProvider.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Data/IStreamDataProvider.cs index 172aea6a7..29c1b4d5c 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Data/IStreamDataProvider.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Data/IStreamDataProvider.cs @@ -6,7 +6,6 @@ namespace Microsoft.Psi.Visualization.Data using System; using System.Collections.Generic; using Microsoft.Psi.Data; - using Microsoft.Psi.Visualization.Helpers; /// /// Defines an interfaces for providers of stream data. @@ -37,9 +36,9 @@ public interface IStreamDataProvider : IDisposable /// Gets the time of the nearest message to a specified time. /// /// The time to find the nearest message to. - /// The type of nearest message to find. + /// The type of nearest message to find. /// The time of the nearest message, if one is found or null otherwise. - DateTime? GetTimeOfNearestMessage(DateTime time, NearestMessageType nearestMessageType); + DateTime? GetTimeOfNearestMessage(DateTime time, NearestType nearestType); /// /// Open stream given a reader. diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Data/StreamAdapter{TSource,TDestination}.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Data/StreamAdapter{TSource,TDestination}.cs index dfa6a6ec5..89877324c 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Data/StreamAdapter{TSource,TDestination}.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Data/StreamAdapter{TSource,TDestination}.cs @@ -10,6 +10,25 @@ namespace Microsoft.Psi.Visualization.Data /// /// The type of the source message. /// The type of the destination message. + /// + /// Apart from providing a method that does the data adaptation, the + /// class (as well as derived classes) also + /// collaboratively contribute to the lifetime management of the objects passing through. + /// Specifically, when implementing a stream adapter, the developer may override + /// the and + /// to enable the data reading + /// layer to use specific allocators (e.g. shared pools) and deallocation procedures for the objects + /// read from disk. In addition, in the + /// + /// method the framework and stream adapters collaboratively ensure that the input parameter remains + /// valid (allocated) throughout the execution of the entire stream adapter chain, all the way to + /// the visualizer. This means that the developer may select a subfield of the input and pass it as + /// output w/o having to clone. At the same time, the developer should not deallocate the input. If + /// however the developer creates new instances of objects in the process of producing an output, + /// then the method should + /// also be overriden and should implement the on-demand disposal of these output objects (which are + /// passed back to this method by the framework.) + /// public abstract class StreamAdapter : IStreamAdapter { /// diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Data/StreamBinding.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Data/StreamBinding.cs index e9e6e12bb..9ccdf0248 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Data/StreamBinding.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Data/StreamBinding.cs @@ -5,43 +5,56 @@ namespace Microsoft.Psi.Visualization.Data { using System; using System.Runtime.Serialization; + using Microsoft.Psi.Visualization.Adapters; /// - /// Represents information needed to uniquely identify and open a stream. + /// Represents information needed to uniquely identify a source or derived stream. /// + /// + /// A stream binding contains an overall stream adapter that is formed by chaining + /// a derived stream adapter (used to compute the values for the derived stream from + /// the source stream) and a visualizer adapter (used to adapt the values produced by + /// the stream to the visualizer that the binding connects to). In addition, the + /// binding contains information about the summarizer used by the bound visualizer. + /// [Serializable] [DataContract] public class StreamBinding { private IStreamAdapter streamAdapter; - private Type streamAdapterType; - private ISummarizer summarizer; - private Type summarizerType; + private IStreamAdapter derivedStreamAdapter; + private Type derivedStreamAdapterType; + private IStreamAdapter visualizerStreamAdapter; + private Type visualizerStreamAdapterType; + private ISummarizer visualizerSummarizer; + private Type visualizerSummarizerType; /// /// Initializes a new instance of the class. /// - /// The stream name. + /// The source stream name. /// The partition name. - /// The path of the node in the tree that has generated this stream binding. - /// The type of the stream adapter, null if there is none. - /// The arguments used when constructing the stream adapter, null if there are none. - /// The type of the stream summarizer, null if there is none. - /// The arguments used when constructing the stream summarizer, null if there are none. - /// True if this stream binding represents a binding to a derived stream rather than to the stream itself, otherwise false. + /// An optional parameter specifying the name of the stream (if not specified, defaults to the source stream name). + /// An optional parameter specifying the type of the stream adapter used to compute the derived stream, null if there is none. + /// An optional parameter specifying the arguments for constructing the stream adapter used to compute the derived stream, null if there are none. + /// An optional parameter specifying the type of the stream adapter used to couple the stream to the visualizer, null if there is none. + /// An optional parameter specifying the arguments for constructing the stream adapter used to couple the stream to the visualizer, null if there are none. + /// An optional parameter specifying the type of the stream summarizer used by the visualizer, null if there is none. + /// An optional parameter specifying the arguments used when constructing the stream summarizer used by the visualizer, null if there are none. public StreamBinding( - string streamName, + string sourceStreamName, string partitionName, - string nodePath, - Type streamAdapterType = null, - object[] streamAdapterArguments = null, - Type summarizerType = null, - object[] summarizerArguments = null, - bool isDerived = false) + string streamName = null, + Type derivedStreamAdapterType = null, + object[] derivedStreamAdapterArguments = null, + Type visualizerStreamAdapterType = null, + object[] visualizerStreamAdapterArguments = null, + Type visualizerSummarizerType = null, + object[] visualizerSummarizerArguments = null) { - if (string.IsNullOrWhiteSpace(streamName)) + if (string.IsNullOrWhiteSpace(sourceStreamName)) { - throw new ArgumentNullException(nameof(streamName)); + throw new ArgumentNullException(nameof(sourceStreamName)); } if (string.IsNullOrWhiteSpace(partitionName)) @@ -49,14 +62,15 @@ public class StreamBinding throw new ArgumentNullException(nameof(partitionName)); } - this.StreamName = streamName; this.PartitionName = partitionName; - this.NodePath = nodePath; - this.StreamAdapterType = streamAdapterType; - this.StreamAdapterArguments = streamAdapterArguments; - this.SummarizerType = summarizerType; - this.SummarizerArguments = summarizerArguments; - this.IsDerived = isDerived; + this.SourceStreamName = sourceStreamName; + this.StreamName = streamName ?? sourceStreamName; + this.DerivedStreamAdapterType = derivedStreamAdapterType; + this.DerivedStreamAdapterArguments = derivedStreamAdapterArguments; + this.VisualizerStreamAdapterType = visualizerStreamAdapterType; + this.VisualizerStreamAdapterArguments = visualizerStreamAdapterArguments; + this.VisualizerSummarizerType = visualizerSummarizerType; + this.VisualizerSummarizerArguments = visualizerSummarizerArguments; } private StreamBinding() @@ -65,149 +79,256 @@ private StreamBinding() } /// - /// Gets stream name. + /// Gets the partition name. /// [DataMember] - public string StreamName { get; internal set; } + public string PartitionName { get; } /// - /// Gets partition name. + /// Gets the source stream name. /// [DataMember] - public string PartitionName { get; private set; } + public string SourceStreamName { get; } /// - /// Gets the node path. + /// Gets the stream name. /// + /// + /// In the case of derived streams, the stream name is different from the source + /// stream name. + /// [DataMember] - public string NodePath { get; private set; } + public string StreamName { get; } /// - /// Gets stream adapter. + /// Gets a value indicating whether the binding is to a derived stream. /// [IgnoreDataMember] + public bool IsBindingToDerivedStream => this.DerivedStreamAdapter != null; + + /// + /// Gets the end-to-end stream adapter. + /// + /// + /// The end-to-end stream adapter for the binding composes any existing + /// derived stream adapter and visualizer adapter. + /// + [IgnoreDataMember] public IStreamAdapter StreamAdapter { get { if (this.streamAdapter == null) { - this.streamAdapter = this.StreamAdapterType != null ? (IStreamAdapter)Activator.CreateInstance(this.StreamAdapterType, this.StreamAdapterArguments) : null; + Type streamAdapterType; + object[] streamAdapterArguments; + if (this.DerivedStreamAdapterType != null) + { + if (this.VisualizerStreamAdapterType != null) + { + streamAdapterType = typeof(ChainedStreamAdapter<,,,,>).MakeGenericType( + this.DerivedStreamAdapter.SourceType, + this.DerivedStreamAdapter.DestinationType, + this.VisualizerStreamAdapter.DestinationType, + this.DerivedStreamAdapterType, + this.VisualizerStreamAdapterType); + streamAdapterArguments = new object[] { this.DerivedStreamAdapterArguments, this.VisualizerStreamAdapterArguments }; + } + else + { + streamAdapterType = this.DerivedStreamAdapterType; + streamAdapterArguments = this.DerivedStreamAdapterArguments; + } + } + else + { + streamAdapterType = this.VisualizerStreamAdapterType; + streamAdapterArguments = this.VisualizerStreamAdapterArguments; + } + + this.streamAdapter = streamAdapterType != null ? (IStreamAdapter)Activator.CreateInstance(streamAdapterType, streamAdapterArguments) : null; } return this.streamAdapter; } + } + + /// + /// Gets the derived stream adapter. + /// + /// + /// The derived stream adapter is used to compute values for the derived + /// stream based on the source stream. + /// + [IgnoreDataMember] + public IStreamAdapter DerivedStreamAdapter + { + get + { + if (this.derivedStreamAdapter == null) + { + this.derivedStreamAdapter = this.DerivedStreamAdapterType != null ? (IStreamAdapter)Activator.CreateInstance(this.DerivedStreamAdapterType, this.DerivedStreamAdapterArguments) : null; + } + + return this.derivedStreamAdapter; + } + } + + /// + /// Gets the derived stream adapter type. + /// + [IgnoreDataMember] + public Type DerivedStreamAdapterType + { + get + { + if (this.derivedStreamAdapterType == null && this.DerivedStreamAdapterTypeName != null) + { + this.derivedStreamAdapterType = TypeResolutionHelper.GetVerifiedType(this.DerivedStreamAdapterTypeName); + } + + return this.derivedStreamAdapterType; + } private set { - // update value and update type (and type name) as well - this.streamAdapter = value; - this.StreamAdapterType = this.streamAdapter?.GetType(); + // update value and update type name + this.derivedStreamAdapterType = value; + + // use assembly-qualified name as stream adapter may be in a different assembly + this.DerivedStreamAdapterTypeName = this.derivedStreamAdapterType?.AssemblyQualifiedName; } } /// - /// Gets or sets the stream adapter arguments needed by the ctor of the stream adapter. + /// Gets the derived stream adapter arguments. /// [DataMember] - public object[] StreamAdapterArguments { get; set; } + public object[] DerivedStreamAdapterArguments { get; } /// - /// Gets a value indicating whether this stream binding represents a binding to a member of the data in the stream rather than to the stream itself. + /// Gets visualizer stream adapter. /// - [DataMember] - public bool IsDerived { get; private set; } + /// + /// The visualizer stream adapter is used to adapt the values of the stream + /// to the visualizer used. + /// + [IgnoreDataMember] + public IStreamAdapter VisualizerStreamAdapter + { + get + { + if (this.visualizerStreamAdapter == null) + { + this.visualizerStreamAdapter = this.VisualizerStreamAdapterType != null ? (IStreamAdapter)Activator.CreateInstance(this.VisualizerStreamAdapterType, this.VisualizerStreamAdapterArguments) : null; + } + + return this.visualizerStreamAdapter; + } + } /// - /// Gets stream adapter type. + /// Gets visualizer stream adapter type. /// [IgnoreDataMember] - public Type StreamAdapterType + public Type VisualizerStreamAdapterType { get { - if (this.streamAdapterType == null && this.StreamAdapterTypeName != null) + if (this.visualizerStreamAdapterType == null && this.VisualizerStreamAdapterTypeName != null) { - this.streamAdapterType = TypeResolutionHelper.GetVerifiedType(this.StreamAdapterTypeName); + this.visualizerStreamAdapterType = TypeResolutionHelper.GetVerifiedType(this.VisualizerStreamAdapterTypeName); } - return this.streamAdapterType; + return this.visualizerStreamAdapterType; } private set { // update value and update type name - this.streamAdapterType = value; + this.visualizerStreamAdapterType = value; // use assembly-qualified name as stream adapter may be in a different assembly - this.StreamAdapterTypeName = this.streamAdapterType?.AssemblyQualifiedName; + this.VisualizerStreamAdapterTypeName = this.visualizerStreamAdapterType?.AssemblyQualifiedName; } } /// - /// Gets summarizer. + /// Gets the visualizer stream adapter arguments. + /// + [DataMember] + public object[] VisualizerStreamAdapterArguments { get; } + + /// + /// Gets the summarizer. /// [IgnoreDataMember] public ISummarizer Summarizer { get { - if (this.summarizer == null) + if (this.visualizerSummarizer == null) { - this.summarizer = this.SummarizerType != null ? (ISummarizer)Activator.CreateInstance(this.SummarizerType, this.SummarizerArguments) : null; + this.visualizerSummarizer = this.VisualizerSummarizerType != null ? (ISummarizer)Activator.CreateInstance(this.VisualizerSummarizerType, this.VisualizerSummarizerArguments) : null; } - return this.summarizer; + return this.visualizerSummarizer; } private set { // update value and update type (and type name) as well - this.summarizer = value; - this.SummarizerType = this.summarizer?.GetType(); + this.visualizerSummarizer = value; + this.VisualizerSummarizerType = this.visualizerSummarizer?.GetType(); } } /// - /// Gets or sets the summarizer arguments. - /// - [DataMember] - public object[] SummarizerArguments { get; set; } - - /// - /// Gets summarizer type. + /// Gets the summarizer type. /// [IgnoreDataMember] - public Type SummarizerType + public Type VisualizerSummarizerType { get { - if (this.summarizerType == null && this.SummarizerTypeName != null) + if (this.visualizerSummarizerType == null && this.SummarizerTypeName != null) { - this.summarizerType = TypeResolutionHelper.GetVerifiedType(this.SummarizerTypeName); + this.visualizerSummarizerType = TypeResolutionHelper.GetVerifiedType(this.SummarizerTypeName); } - return this.summarizerType; + return this.visualizerSummarizerType; } private set { // update value and update type name - this.summarizerType = value; + this.visualizerSummarizerType = value; // use assembly-qualified name as stream reader may be in a different assembly - this.SummarizerTypeName = this.summarizerType?.AssemblyQualifiedName; + this.SummarizerTypeName = this.visualizerSummarizerType?.AssemblyQualifiedName; } } /// - /// Gets or sets stream adapter type name. + /// Gets the summarizer arguments. + /// + [DataMember] + public object[] VisualizerSummarizerArguments { get; } + + /// + /// Gets or sets the derived stream adapter type name. + /// + [DataMember] + private string DerivedStreamAdapterTypeName { get; set; } + + /// + /// Gets or sets the visualizer stream adapter type name. /// [DataMember] - private string StreamAdapterTypeName { get; set; } + private string VisualizerStreamAdapterTypeName { get; set; } /// - /// Gets or sets summarizer type name. + /// Gets or sets the summarizer type name. /// [DataMember] private string SummarizerTypeName { get; set; } diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Data/StreamDataProvider{T}.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Data/StreamDataProvider{T}.cs index 698e2ade5..d3add2d61 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Data/StreamDataProvider{T}.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Data/StreamDataProvider{T}.cs @@ -8,7 +8,6 @@ namespace Microsoft.Psi.Visualization.Data using System.Collections.ObjectModel; using System.Runtime.Serialization; using Microsoft.Psi.Data; - using Microsoft.Psi.Visualization.Helpers; /// /// Represents an object used to read streams. @@ -102,7 +101,7 @@ public void RemoveReadRequest(DateTime startTime, DateTime endTime) public abstract void OpenStream(IStreamReader streamReader); /// - public abstract DateTime? GetTimeOfNearestMessage(DateTime time, NearestMessageType snappingBehavior); + public abstract DateTime? GetTimeOfNearestMessage(DateTime time, NearestType nearestType); /// /// Called by a derived class when it no longer has any subscribers. diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Data/StreamIntervalProvider.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Data/StreamIntervalProvider.cs index e49ac9b55..27e94c769 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Data/StreamIntervalProvider.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Data/StreamIntervalProvider.cs @@ -99,9 +99,9 @@ public override void OpenStream(IStreamReader streamReader) } /// - public override DateTime? GetTimeOfNearestMessage(DateTime time, NearestMessageType snappingBehavior) + public override DateTime? GetTimeOfNearestMessage(DateTime time, NearestType nearestType) { - int index = IndexHelper.GetIndexForTime(time, this.data.Count, (idx) => this.data[idx].OriginatingTime, snappingBehavior); + int index = IndexHelper.GetIndexForTime(time, this.data.Count, (idx) => this.data[idx].OriginatingTime, nearestType); return (index >= 0) ? this.data[index].OriginatingTime : null; } diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Data/StreamValueProvider{TSource}.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Data/StreamValueProvider{TSource}.cs index bb7d83e72..9dc93da14 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Data/StreamValueProvider{TSource}.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Data/StreamValueProvider{TSource}.cs @@ -251,9 +251,9 @@ public void ReadAndPublishStreamValue(IStreamReader streamReader, DateTime dateT streamReader.OpenStreamIndex(this.StreamName, this.OnReceiveIndex, this.Allocator); /// - public override DateTime? GetTimeOfNearestMessage(DateTime time, NearestMessageType snappingBehavior) + public override DateTime? GetTimeOfNearestMessage(DateTime time, NearestType nearestType) { - int index = IndexHelper.GetIndexForTime(time, this.indexView.Count, (idx) => this.indexView[idx].OriginatingTime, snappingBehavior); + int index = IndexHelper.GetIndexForTime(time, this.indexView.Count, (idx) => this.indexView[idx].OriginatingTime, nearestType); return (index >= 0) ? this.indexView[index].OriginatingTime : null; } diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/DataTypes/ScriptGlobals.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/DataTypes/ScriptGlobals.cs new file mode 100644 index 000000000..fbbf4648f --- /dev/null +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/DataTypes/ScriptGlobals.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.Psi.Visualization.DataTypes +{ + /// + /// Represents the globals for a script. + /// + /// The type of the stream on which the script will operate. + public class ScriptGlobals + { + /// + /// Initializes a new instance of the class. + /// + /// The message. + /// The envelope. + public ScriptGlobals(T message, Envelope env) + { + this.m = message; + this.e = env; + } + +#pragma warning disable SA1300 // Element should begin with an uppercase letter + /// + /// Gets the message. + /// + public T m { get; } + + /// + /// Gets the envelope. + /// + public Envelope e { get; } +#pragma warning restore SA1300 // Element should begin with an uppercase letter + } +} diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Helpers/CollectionHelper.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Helpers/CollectionHelper.cs new file mode 100644 index 000000000..1ec5ecff2 --- /dev/null +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Helpers/CollectionHelper.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.Psi.Visualization.Helpers +{ + using System; + using System.Collections.Generic; + + /// + /// Helper methods for collections. + /// + internal static class CollectionHelper + { + /// + /// Inserts an item into a list in sorted order based on the supplied comparison method. + /// + /// The type of the list items. + /// The list in which to insert the item. + /// The item to insert. + /// An optional comparison method for determining where to insert the item. + /// If null, the default comparer for the item type is used. + internal static void InsertSorted(this IList list, T item, Comparison comparison = null) + { + if (list.Count == 0) + { + list.Add(item); + } + else + { + comparison ??= Comparer.Default.Compare; + + int index = 0; + while (index < list.Count && comparison(list[index], item) < 1) + { + index++; + } + + list.Insert(index, item); + } + } + } +} diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Helpers/DateTimeFormatHelper.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Helpers/DateTimeFormatHelper.cs deleted file mode 100644 index 7e2d22bfd..000000000 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Helpers/DateTimeFormatHelper.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. - -namespace Microsoft.Psi.Visualization.Helpers -{ - using System; - - /// - /// Represents a string formatter for objects. - /// - public static class DateTimeFormatHelper - { - private const string DateTimeFormat = "MM/dd/yyyy HH:mm:ss.ffff"; - private const string TimeFormat = "HH:mm:ss.ffff"; - - /// - /// Formats a nullable datetime object into a string. - /// - /// The nullable datetime object to format. - /// If true, then DateTime.MinValue and DateTimeMaxValue are rendered explicitly, otherwise they are rendered as empty strings. - /// A string representation of the datetime. - public static string FormatDateTime(DateTime? dateTime, bool renderDateTimeMinMax = true) - { - if (!dateTime.HasValue || (!renderDateTimeMinMax && (dateTime.Value == DateTime.MinValue || dateTime.Value == DateTime.MaxValue))) - { - return string.Empty; - } - - return dateTime.Value.ToString(DateTimeFormat); - } - - /// - /// Formats a nullable time object into a string. - /// - /// The nullable datetime object to format. - /// If true, then DateTime.MinValue and DateTimeMaxValue are rendered explicitly, otherwise they are rendered as empty strings. - /// A string representation of the datetime. - public static string FormatTime(DateTime? dateTime, bool renderDateTimeMinMax = true) - { - if (!dateTime.HasValue || (!renderDateTimeMinMax && (dateTime.Value == DateTime.MinValue || dateTime.Value == DateTime.MaxValue))) - { - return string.Empty; - } - - return dateTime.Value.ToString(TimeFormat); - } - } -} diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Helpers/DateTimeHelper.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Helpers/DateTimeHelper.cs new file mode 100644 index 000000000..4aadbe8ec --- /dev/null +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Helpers/DateTimeHelper.cs @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.Psi.Visualization.Helpers +{ + using System; + using System.Collections.Generic; + + /// + /// Represents a string formatter for objects. + /// + public static class DateTimeHelper + { + private const string DateTimeFormat = "MM/dd/yyyy HH:mm:ss.ffff"; + private const string TimeFormat = "HH:mm:ss.ffff"; + + /// + /// Formats a nullable datetime object into a string. + /// + /// The nullable datetime object to format. + /// If true, then DateTime.MinValue and DateTimeMaxValue are rendered explicitly, otherwise they are rendered as empty strings. + /// A string representation of the datetime. + public static string FormatDateTime(DateTime? dateTime, bool renderDateTimeMinMax = true) + { + if (!dateTime.HasValue || (!renderDateTimeMinMax && (dateTime.Value == DateTime.MinValue || dateTime.Value == DateTime.MaxValue))) + { + return string.Empty; + } + + return dateTime.Value.ToString(DateTimeFormat); + } + + /// + /// Formats a nullable time object into a string. + /// + /// The nullable datetime object to format. + /// If true, then DateTime.MinValue and DateTimeMaxValue are rendered explicitly, otherwise they are rendered as empty strings. + /// A string representation of the datetime. + public static string FormatTime(DateTime? dateTime, bool renderDateTimeMinMax = true) + { + if (!dateTime.HasValue || (!renderDateTimeMinMax && (dateTime.Value == DateTime.MinValue || dateTime.Value == DateTime.MaxValue))) + { + return string.Empty; + } + + return dateTime.Value.ToString(TimeFormat); + } + + /// + /// Computes the minimum of a pair of instances. + /// + /// The first instance. + /// The second instance. + /// The minimum of the pair of instances. + internal static DateTime? MinDateTime(DateTime? first, DateTime? second) + { + if (first == null) + { + return second; + } + else if (second == null) + { + return first; + } + else + { + return new (Math.Min(first.Value.Ticks, second.Value.Ticks)); + } + } + + /// + /// Computes the minimum of an enumerable of instances. + /// + /// The enumerable of instances. + /// The minimum of the enumerable of instances. + internal static DateTime? MinDateTime(IEnumerable enumerable) + { + var result = default(DateTime?); + + foreach (var item in enumerable) + { + result = MinDateTime(result, item); + } + + return result; + } + + /// + /// Computes the maximum of a pair of instances. + /// + /// The first instance. + /// The second instance. + /// The maximum of the pair of instances. + internal static DateTime? MaxDateTime(DateTime? first, DateTime? second) + { + if (first == null) + { + return second; + } + else if (second == null) + { + return first; + } + else + { + return new (Math.Max(first.Value.Ticks, second.Value.Ticks)); + } + } + + /// + /// Computes the maximum of an enumerable of instances. + /// + /// The enumerable of instances. + /// The maximum of the enumerable of instances. + internal static DateTime? MaxDateTime(IEnumerable enumerable) + { + var result = default(DateTime?); + + foreach (var item in enumerable) + { + result = MaxDateTime(result, item); + } + + return result; + } + } +} diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Helpers/SizeFormatHelper.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Helpers/SizeHelper.cs similarity index 68% rename from Sources/Visualization/Microsoft.Psi.Visualization.Windows/Helpers/SizeFormatHelper.cs rename to Sources/Visualization/Microsoft.Psi.Visualization.Windows/Helpers/SizeHelper.cs index 9be74b3bf..357f8e124 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Helpers/SizeFormatHelper.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Helpers/SizeHelper.cs @@ -3,10 +3,12 @@ namespace Microsoft.Psi.Visualization.Helpers { + using System.Collections.Generic; + /// /// Implements a size formatter for data size objects. /// - internal static class SizeFormatHelper + internal static class SizeHelper { /// /// Formats a data size specified as a long into a string, e.g. 23.1 MB, etc. @@ -91,5 +93,44 @@ public static string FormatThroughput(double throughput, string timeUnit) return $"{throughput / 1000000000000.0:0.0} T/{timeUnit}"; } } + + /// + /// Computes the sum of a pair of nullable long instances. + /// + /// The first nullable long instance. + /// The second nullable long instance. + /// The maximum of the pair of nullable long instances. + internal static long? NullableSum(long? first, long? second) + { + if (first == null) + { + return second; + } + else if (second == null) + { + return first; + } + else + { + return first.Value + second.Value; + } + } + + /// + /// Computes the sum of an enumerable of nullable long instances. + /// + /// The enumerable of nullable long instances. + /// The maximum of the enumerable of nullable long instances. + internal static long? NullableSum(IEnumerable enumerable) + { + var result = default(long?); + + foreach (var item in enumerable) + { + result = NullableSum(result, item); + } + + return result; + } } } diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Helpers/TimeSpanFormatHelper.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Helpers/TimeSpanHelper.cs similarity index 97% rename from Sources/Visualization/Microsoft.Psi.Visualization.Windows/Helpers/TimeSpanFormatHelper.cs rename to Sources/Visualization/Microsoft.Psi.Visualization.Windows/Helpers/TimeSpanHelper.cs index 0061e7d1e..bdcb7a982 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Helpers/TimeSpanFormatHelper.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Helpers/TimeSpanHelper.cs @@ -8,7 +8,7 @@ namespace Microsoft.Psi.Visualization.Helpers /// /// Represents a string formatter for objects. /// - public static class TimeSpanFormatHelper + public static class TimeSpanHelper { /// /// Formats a time span as an readable (approximate value). diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals2D/DiagnosticsVisualization/TypeSpec.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Helpers/TypeSpec.cs similarity index 93% rename from Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals2D/DiagnosticsVisualization/TypeSpec.cs rename to Sources/Visualization/Microsoft.Psi.Visualization.Windows/Helpers/TypeSpec.cs index ab2dcd0e0..24780a90e 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals2D/DiagnosticsVisualization/TypeSpec.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Helpers/TypeSpec.cs @@ -78,6 +78,22 @@ public static string Simplify(string typeName) return new TypeSpec(typeName).ToString(); } + /// + /// Returns a string representation of the type as could be written in code. + /// + /// The type. + /// The friendly name of the type. + public static string GetCodeFriendlyName(Type type) + { + string fullName = type.Name; + if (type.IsGenericType) + { + return fullName.Split('`')[0] + "<" + string.Join(", ", type.GetGenericArguments().Select(t => TypeSpec.GetCodeFriendlyName(t)).ToArray()) + ">"; + } + + return fullName; + } + /// public override string ToString() { @@ -142,8 +158,8 @@ private static IEnumerable Lex(string value) /// Type specification. private static TypeSpec Parse(Token[] tokens) { - StringBuilder name = new StringBuilder(); - StringBuilder modifier = new StringBuilder(); + var name = new StringBuilder(); + var modifier = new StringBuilder(); for (var i = 0; i < tokens.Length; i++) { var t = tokens[i]; diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/IContextMenuItemsSource.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/IContextMenuItemsSource.cs new file mode 100644 index 000000000..6916962cc --- /dev/null +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/IContextMenuItemsSource.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.Psi.Visualization +{ + using System.Collections.Generic; + + /// + /// Represents an object that is capable of supplying context menu items. + /// + public interface IContextMenuItemsSource + { + /// + /// Gets the set of context menu items. + /// + /// + /// The set of context menu items. + /// + List ContextMenuItemsInfo(); + } +} diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Icons/set-annotation-to-selection.png b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Icons/set-annotation-to-selection.png new file mode 100644 index 0000000000000000000000000000000000000000..cf7328af025204fc4b06693ae499936ef7ec8900 GIT binary patch literal 1465 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sDEfH31!Z9ZwB;1W+4#}EtuD{v*s6D!Eu<`1oXX16LKiz-jGl!PyYyPV)Xvq)m zLDp4Un(*fG3fAfsZzA{U%S&mAB+M(n=k`nS?2$VyEYFwcOt#Oy|NZ0r{Tmzf0xr9R zcl={8I93y8lc6yc*_4cuk^(Dz{ese>9KHOabp4cM{nVV)+|<01VtqqBLwzNMirk#M zVyl9T{F40QjQj#yC8(CXV!gza{G?R9Zh@~ab`2GY1z@GQxp39R zC9Y*9_*EB&R2HP_2c;J0mlh?bx|RW*sSI*qft7PnYGO%#QAmD%j;)d-$XyBnFefWG zJ1ZC&7+NZ5_$DT2=7Id7iKHvlC9x#cRtcm+*T78I$RfnR(#pul%EUm|z|_jX0HV}4 zKP5A*5}Q&J11l2?6s4&pi7AOCi3Am=BwMBB7v&}beVv(`n4YR%ke9Bc01rzm=lq=f zBA~@U*BK*=dpbJ@6y>L7<^Ux?*+jw9)!EF@(hTT$Jwvc_eSNJw^NLFn^O93NU2K(r zA*7d?nPO#ZmSkyRY;35ToMe%xYhq!Xs%w#IkgA(zV3L$#keHI1YH0-1?~$nT--p+3M-(7Twp2#a_lP#5|gu2OB7P`Qf!rqlzs=#Chb>0sv~Q=ac{d literal 0 HcmV?d00001 diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Microsoft.Psi.Visualization.Windows.csproj b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Microsoft.Psi.Visualization.Windows.csproj index 604c95292..f302bc3cd 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Microsoft.Psi.Visualization.Windows.csproj +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Microsoft.Psi.Visualization.Windows.csproj @@ -133,6 +133,7 @@ + @@ -168,6 +169,7 @@ + @@ -226,11 +228,11 @@ - + @@ -240,12 +242,14 @@ + + all runtime; build; native; contentfiles; analyzers @@ -277,7 +281,8 @@ - + + @@ -301,12 +306,12 @@ - - - - - - + + + + + + @@ -335,12 +340,12 @@ - + @@ -349,10 +354,12 @@ + + diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Navigation/Navigator.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Navigation/Navigator.cs index a78b972ed..a07c5d897 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Navigation/Navigator.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Navigation/Navigator.cs @@ -280,12 +280,12 @@ public double PlaySpeed /// /// Gets selection start as a formatted string. /// - public string SelectionStartFormatted => DateTimeFormatHelper.FormatDateTime(this.SelectionRange.StartTime, false); + public string SelectionStartFormatted => DateTimeHelper.FormatDateTime(this.SelectionRange.StartTime, false); /// /// Gets selection end as a formatted string. /// - public string SelectionEndFormatted => DateTimeFormatHelper.FormatDateTime(this.SelectionRange.EndTime, false); + public string SelectionEndFormatted => DateTimeHelper.FormatDateTime(this.SelectionRange.EndTime, false); /// /// Gets the offset of the section start from the start of the ession. diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/PluginMap.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/PluginMap.cs index 1151819aa..c9c1393d0 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/PluginMap.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/PluginMap.cs @@ -45,6 +45,9 @@ public class PluginMap // This list of stream readers that were found during discovery. private readonly List<(string Name, string Extension, Type ReaderType)> streamReaders = new (); + // This dictionary provides a set of additional type mappings. + private readonly Dictionary additionalTypeMappings = new (); + /// /// Gets a value indicating whether or not Initialize() has been called. /// @@ -55,6 +58,11 @@ public class PluginMap /// public IReadOnlyList Visualizers => this.visualizers.AsReadOnly(); + /// + /// Gets the set of additional type mappings. + /// + public IReadOnlyDictionary AdditionalTypeMappings => this.additionalTypeMappings; + /// /// Gets the set of available batch processing tasks. /// @@ -75,8 +83,16 @@ public class PluginMap /// /// A list of assemblies to search for plugins in addition to the default assembly list. /// If no additional assemblies contain plugins, this parameter can be null or an empty list. + /// A set of additional type mappings. /// The full path to the log file to be created and written to while initializing. - public void Initialize(List additionalAssembliesToSearch, string loadLogFilename) + /// Indicates whether to show the error log. + /// The folder under which batch processing task configurations are saved. + public void Initialize( + List additionalAssembliesToSearch, + Dictionary additionalTypeMappings, + string loadLogFilename, + bool showErrorLog, + string batchProcessingTaskConfigurationsPath) { // Append the additional assemblies to the list of default assemblies to search for plugins List assembliesToSearch = this.defaultAssemblies.ToList(); @@ -86,7 +102,17 @@ public void Initialize(List additionalAssembliesToSearch, string loadLog } // Load all the visualizers, summarizers, stream adapters, stream readers, and batch processing tasks - this.DiscoverPlugins(assembliesToSearch, loadLogFilename); + this.DiscoverPlugins(assembliesToSearch, loadLogFilename, showErrorLog, batchProcessingTaskConfigurationsPath); + + // Set up the additional type mappings + foreach (var kvp in additionalTypeMappings) + { + var verifiedType = TypeResolutionHelper.GetVerifiedType(kvp.Value); + if (verifiedType != null) + { + this.additionalTypeMappings.Add(kvp.Key, verifiedType); + } + } this.IsInitialized = true; } @@ -110,7 +136,7 @@ public Type GetStreamReaderType(string extension) return this.StreamReaders.Where(sr => sr.Extension == extension).First().ReaderType; } - private void DiscoverPlugins(List assemblies, string loadLogFilename) + private void DiscoverPlugins(List assemblies, string loadLogFilename, bool showErrorLog, string batchProcessingTaskConfigurationsPath) { bool hasErrors = false; @@ -172,7 +198,7 @@ private void DiscoverPlugins(List assemblies, string loadLogFilename) { foreach (var attr in type.GetCustomAttributes(typeof(BatchProcessingTaskAttribute))) { - this.AddBatchProcessingTask(type, (BatchProcessingTaskAttribute)attr, logWriter, assemblyPath); + this.AddBatchProcessingTask(type, (BatchProcessingTaskAttribute)attr, logWriter, assemblyPath, batchProcessingTaskConfigurationsPath); } } @@ -187,7 +213,7 @@ private void DiscoverPlugins(List assemblies, string loadLogFilename) { foreach (var attr in method.GetCustomAttributes(typeof(BatchProcessingTaskAttribute))) { - this.AddBatchProcessingTask(method, (BatchProcessingTaskAttribute)attr, logWriter, assemblyPath); + this.AddBatchProcessingTask(method, (BatchProcessingTaskAttribute)attr, logWriter, assemblyPath, batchProcessingTaskConfigurationsPath); } } } @@ -195,7 +221,7 @@ private void DiscoverPlugins(List assemblies, string loadLogFilename) } // Load all of the visualization object types that were found earlier - Dictionary.Enumerator visualizationObjectTypesEnumerator = visualizationObjectTypes.GetEnumerator(); + var visualizationObjectTypesEnumerator = visualizationObjectTypes.GetEnumerator(); while (visualizationObjectTypesEnumerator.MoveNext()) { this.AddVisualizer(visualizationObjectTypesEnumerator.Current.Key, logWriter, visualizationObjectTypesEnumerator.Current.Value); @@ -209,7 +235,7 @@ private void DiscoverPlugins(List assemblies, string loadLogFilename) } // If there were any errors while loading the visualizers etc, inform the user and allow him to view the log. - if (hasErrors) + if (hasErrors && showErrorLog) { var messageBoxWindow = new MessageBoxWindow( Application.Current.MainWindow, @@ -221,7 +247,7 @@ private void DiscoverPlugins(List assemblies, string loadLogFilename) if (messageBoxWindow.ShowDialog() == true) { // Display the log file in the default application for text files. - Process.Start(loadLogFilename); + Process.Start("notepad.exe", loadLogFilename); } } } @@ -264,10 +290,10 @@ private void AddVisualizer(Type visualizationObjectType, VisualizationLogWriter { // Add both a "Visualize" and a "Visualize in new panel" command metadata logWriter.WriteLine("Loading Visualizer {0} from {1}...", visualizationObjectType.Name, assemblyPath); - List visualizerMetadata = VisualizerMetadata.Create(visualizationObjectType, this.summarizers, this.streamAdapters, logWriter); - if (visualizerMetadata != null) + var visualizer = VisualizerMetadata.Create(visualizationObjectType, this.summarizers, this.streamAdapters, logWriter); + if (visualizer != null) { - this.visualizers.AddRange(visualizerMetadata); + this.visualizers.AddRange(visualizer); } } @@ -295,10 +321,11 @@ private void AddStreamAdapter(Type adapterType, VisualizationLogWriter logWriter Type batchProcessingTaskType, BatchProcessingTaskAttribute batchProcessingTaskAttribute, VisualizationLogWriter logWriter, - string assemblyPath) + string assemblyPath, + string batchProcessingTaskConfigurationsPath) { logWriter.WriteLine("Loading Batch Processing Task {0} from {1}...", batchProcessingTaskAttribute.Name, assemblyPath); - var batchProcessingTaskMetadata = new BatchProcessingTaskMetadata(batchProcessingTaskType, batchProcessingTaskAttribute); + var batchProcessingTaskMetadata = new BatchProcessingTaskMetadata(batchProcessingTaskType, batchProcessingTaskAttribute, batchProcessingTaskConfigurationsPath); if (batchProcessingTaskMetadata != null) { this.batchProcessingTasks.Add(batchProcessingTaskMetadata); @@ -309,10 +336,11 @@ private void AddStreamAdapter(Type adapterType, VisualizationLogWriter logWriter MethodInfo batchProcessingMethodInfo, BatchProcessingTaskAttribute batchProcessingTaskAttribute, VisualizationLogWriter logWriter, - string assemblyPath) + string assemblyPath, + string batchProcessingTaskConfigurationsPath) { logWriter.WriteLine("Loading Batch Processing Task {0} from {1}...", batchProcessingTaskAttribute.Name, assemblyPath); - var batchProcessingTaskMetadata = new BatchProcessingTaskMetadata(batchProcessingMethodInfo, batchProcessingTaskAttribute); + var batchProcessingTaskMetadata = new BatchProcessingTaskMetadata(batchProcessingMethodInfo, batchProcessingTaskAttribute, batchProcessingTaskConfigurationsPath); if (batchProcessingTaskMetadata != null) { this.batchProcessingTasks.Add(batchProcessingTaskMetadata); diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/PropertyGridEditors/ItemPickerEditor.xaml b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/PropertyGridEditors/ItemPickerEditor.xaml new file mode 100644 index 000000000..8d05e710d --- /dev/null +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/PropertyGridEditors/ItemPickerEditor.xaml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/PropertyGridEditors/ItemPickerEditor.xaml.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/PropertyGridEditors/ItemPickerEditor.xaml.cs new file mode 100644 index 000000000..9627e0d3f --- /dev/null +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/PropertyGridEditors/ItemPickerEditor.xaml.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.Psi.Visualization.PropertyGridEditors +{ + using System.Windows; + using System.Windows.Controls; + using Xceed.Wpf.Toolkit.PropertyGrid; + using Xceed.Wpf.Toolkit.PropertyGrid.Editors; + + /// + /// Interaction logic for ItemPickerEditor.xaml. + /// + public partial class ItemPickerEditor : UserControl, ITypeEditor + { + /// + /// Initializes a new instance of the class. + /// + public ItemPickerEditor() + { + this.InitializeComponent(); + } + + /// + public FrameworkElement ResolveEditor(PropertyItem propertyItem) + { + // Set the editor's data context to the ItemSelectorViewModel property + this.DataContext = propertyItem.Value; + return this; + } + } +} \ No newline at end of file diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/PropertyGridEditors/ItemPickerViewModel.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/PropertyGridEditors/ItemPickerViewModel.cs new file mode 100644 index 000000000..7ec3c6268 --- /dev/null +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/PropertyGridEditors/ItemPickerViewModel.cs @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.Psi.Visualization.PropertyGridEditors +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Runtime.Serialization; + using Microsoft.Psi.Data; + + /// + /// Provides a view model for selecting a value from an enumeration of items. + /// + /// The type of the items. + /// + /// Add a property of this type to display a dropdown list picker in the PropertyGrid which supports + /// selecting one of an enumeration of items of type . The property should be + /// decorated with the specifying an editor type + /// of . + /// + public class ItemPickerViewModel : ObservableObject + { + private IEnumerable items = default; + private T selectedItem = default; + private Func displayNameSelector = o => o.ToString(); + + /// + /// Initializes a new instance of the class. + /// + /// A function to extract the unique display name from each list item. + public ItemPickerViewModel(Func displayNameSelector = null) + { + this.displayNameSelector = displayNameSelector ?? (o => o.ToString()); + } + + /// + /// Gets or sets the enumeration of items. + /// + [DataMember] + public IEnumerable Items + { + get { return this.items; } + + set + { + this.Set(nameof(this.Items), ref this.items, value); + + // Save the current selection before raising property changed (which may cause SelectedItem to be cleared) + string currentSelection = this.SelectedItem is not null ? this.displayNameSelector(this.SelectedItem) : null; + this.RaisePropertyChanged(nameof(this.ItemDisplayNames)); + + // Re-bind SelectedItem to the first item in the new list with the same display name (or the first item if currentSelection is null) + this.SelectedItem = currentSelection is not null ? + this.Items.FirstOrDefault(value => this.displayNameSelector(value) == currentSelection) : + this.Items.FirstOrDefault(); + } + } + + /// + /// Gets the display names for the enumeration of items. Display names should be unique. + /// + public System.Collections.IEnumerable ItemDisplayNames + => this.items?.Select(item => new { DisplayName = this.displayNameSelector(item), Value = item }); + + /// + /// Gets or sets the selected item. + /// + [DataMember] + public T SelectedItem + { + get { return this.selectedItem; } + set { this.Set(nameof(this.SelectedItem), ref this.selectedItem, value); } + } + } +} diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Themes/PropertyGrid.xaml b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Themes/PropertyGrid.xaml index 0d4fdd731..331f92b32 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Themes/PropertyGrid.xaml +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Themes/PropertyGrid.xaml @@ -1097,11 +1097,11 @@ Value="{Binding Description, RelativeSource={RelativeSource TemplatedParent}}" TargetName="PART_Name" /> - + diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/TypeKeyedActionCommand.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/TypeKeyedActionCommand.cs deleted file mode 100644 index 9f17a2693..000000000 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/TypeKeyedActionCommand.cs +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. - -#pragma warning disable CS0067 // The event 'TypeKeyedActionCommand.CanExecuteChanged' is never used. - -namespace Microsoft.Psi.Visualization.Commands -{ - using System; - using System.Windows.Input; - - /// - /// Base class for context menu commands based on stream types. - /// - public abstract class TypeKeyedActionCommand : ICommand - { - private string displayName; - private Type typeKey; - private string icon; - - /// - /// Initializes a new instance of the class. - /// - /// Name displayed in menu. - /// Type key of the command. - /// The path to the icon to display next to the menu. - public TypeKeyedActionCommand(string displayName, Type typeKey, string icon) - { - this.displayName = displayName; - this.typeKey = typeKey; - this.icon = icon; - } - - /// - public event EventHandler CanExecuteChanged - { - // event is never raised - no-op accessor prevents CS0067 warning - add { } - remove { } - } - - /// - /// Gets the display name. - /// - public string DisplayName => this.displayName; - - /// - /// Gets the icon for the command. - /// - public string Icon => this.icon; - - /// - /// Gets the type key. - /// - public Type TypeKey => this.typeKey; - - /// - public bool CanExecute(object parameter) - { - return true; - } - - /// - public abstract void Execute(object parameter); - } -} diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/TypeKeyedActionCommand{TKey,TParam}.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/TypeKeyedActionCommand{TKey,TParam}.cs deleted file mode 100644 index 810c3ae37..000000000 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/TypeKeyedActionCommand{TKey,TParam}.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. - -namespace Microsoft.Psi.Visualization.Commands -{ - using System; - - /// - /// Class for context menu commands based on stream types. - /// - /// The type of key. - /// The type of parameter passed to action. - public class TypeKeyedActionCommand : TypeKeyedActionCommand - { - private Action action; - - /// - /// Initializes a new instance of the class. - /// - /// Name displayed in menu. - /// Action to be invoked when menu is clicked. - /// The path to the icon to display next to the menu. - public TypeKeyedActionCommand(string displayName, Action action, string icon) - : base(displayName, typeof(TKey), icon) - { - this.action = action; - } - - /// - public override void Execute(object parameter) - { - this.action((TParam)parameter); - } - } -} diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/ViewModels/DatasetViewModel.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/ViewModels/DatasetViewModel.cs index 19a30c03e..d22805090 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/ViewModels/DatasetViewModel.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/ViewModels/DatasetViewModel.cs @@ -875,6 +875,42 @@ public void RemoveSession(SessionViewModel sessionViewModel) } } + /// + /// Removes a partition specified by name from all sessions. + /// + /// The partition name. + public void RemovePartitionFromAllSessions(string partitionName) + { + var count = this.sessionViewModels.Count(svm => svm.PartitionViewModels.Any(pvm => pvm.Name == partitionName)); + if (count > 1) + { + var result = new MessageBoxWindow( + Application.Current.MainWindow, + "Are you sure?", + $"The partition named {partitionName} appears in {count} sessions. Are you sure you want to remove it from all these sessions?", + "Close", + null) + .ShowDialog(); + + if (result == null || !result.Value) + { + return; + } + } + + foreach (var sessionViewModel in this.sessionViewModels) + { + var partitionViewModel = sessionViewModel.PartitionViewModels.FirstOrDefault(p => p.Name == partitionName); + if (partitionViewModel != null) + { + if (partitionViewModel.PromptSaveChangesAndContinue()) + { + sessionViewModel.RemovePartition(partitionViewModel); + } + } + } + } + /// public override string ToString() => "Dataset: " + this.Name; @@ -1035,16 +1071,16 @@ private void UpdateAuxiliaryInfo() this.AuxiliaryInfo = this.OriginatingTimeInterval.Left.ToLocalTime().ToString(); break; case AuxiliaryDatasetInfo.DataThroughputPerHour: - this.AuxiliaryInfo = this.Dataset.Size.HasValue ? SizeFormatHelper.FormatThroughput(this.Dataset.Size.Value / this.TotalDuration.TotalHours, "hour") : "?"; + this.AuxiliaryInfo = this.Dataset.Size.HasValue ? SizeHelper.FormatThroughput(this.Dataset.Size.Value / this.TotalDuration.TotalHours, "hour") : "?"; break; case AuxiliaryDatasetInfo.DataThroughputPerMinute: - this.AuxiliaryInfo = this.Dataset.Size.HasValue ? SizeFormatHelper.FormatThroughput(this.Dataset.Size.Value / this.TotalDuration.TotalMinutes, "min") : "?"; + this.AuxiliaryInfo = this.Dataset.Size.HasValue ? SizeHelper.FormatThroughput(this.Dataset.Size.Value / this.TotalDuration.TotalMinutes, "min") : "?"; break; case AuxiliaryDatasetInfo.DataThroughputPerSecond: - this.AuxiliaryInfo = this.Dataset.Size.HasValue ? SizeFormatHelper.FormatThroughput(this.Dataset.Size.Value / this.TotalDuration.TotalSeconds, "sec") : "?"; + this.AuxiliaryInfo = this.Dataset.Size.HasValue ? SizeHelper.FormatThroughput(this.Dataset.Size.Value / this.TotalDuration.TotalSeconds, "sec") : "?"; break; case AuxiliaryDatasetInfo.Size: - this.AuxiliaryInfo = this.Dataset.Size.HasValue ? SizeFormatHelper.FormatSize(this.Dataset.Size.Value) : "?"; + this.AuxiliaryInfo = this.Dataset.Size.HasValue ? SizeHelper.FormatSize(this.Dataset.Size.Value) : "?"; break; case AuxiliaryDatasetInfo.StreamCount: this.AuxiliaryInfo = this.Dataset.StreamCount.HasValue ? (this.Dataset.StreamCount == 0 ? "0" : $"{this.Dataset.StreamCount.Value:0,0.}") : "?"; diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/ViewModels/DerivedMemberStreamTreeNode.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/ViewModels/DerivedMemberStreamTreeNode.cs deleted file mode 100644 index 27ec75524..000000000 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/ViewModels/DerivedMemberStreamTreeNode.cs +++ /dev/null @@ -1,207 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. - -namespace Microsoft.Psi.Visualization.ViewModels -{ - using System; - using System.Collections.Generic; - using System.ComponentModel; - using System.Linq; - using System.Reflection; - using Microsoft.Psi.Visualization.Adapters; - using Microsoft.Psi.Visualization.Data; - using Microsoft.Psi.Visualization.VisualizationObjects; - - /// - /// Implements a node in the dataset tree that represents a derived member stream, - /// i.e. contains data corresponding to a derived field of property of a stream. - /// - public class DerivedMemberStreamTreeNode : DerivedStreamTreeNode - { - /// - /// Initializes a new instance of the class. - /// - /// The parent stream tree node. - /// The member information. - /// The type of the member. - /// Indicates whether this is an auto-generated nullable expansion. - public DerivedMemberStreamTreeNode(StreamTreeNode parent, MemberInfo memberInfo, Type memberType, bool generateNullable) - : base( - parent.PartitionViewModel, - $"{parent.Path}.{memberInfo.Name}", - memberInfo.Name, - parent.SourceStreamMetadata) - { - this.DataTypeName = - generateNullable ? - typeof(Nullable<>).MakeGenericType(memberType).AssemblyQualifiedName : - memberType.AssemblyQualifiedName; - this.MemberPath = - parent is DerivedMemberStreamTreeNode expandedMemberParent ? - $"{expandedMemberParent.MemberPath}.{memberInfo.Name}" : - memberInfo.Name; - this.IsAutoGeneratedNullableMember = generateNullable; - } - - /// - /// Gets the member path of this node relative to source stream node. - /// - [DisplayName("Member Path")] - [Description("The path from the messages in the stream to this property or field member")] - public string MemberPath { get; private set; } - - /// - /// Gets or sets a value indicating whether this stream is an auto-generated nullable member. - /// - /// When derived members are generated for value types, if any of the ancestor - /// streams are of a reference type, the derived member will have a corresponding nullable value type, - /// instead of the actual value type. This is done so that values for the member can be computed - /// even in the case when the ancestor object is null. - [Browsable(false)] - public bool IsAutoGeneratedNullableMember { get; protected set; } - - /// - public override StreamBinding CreateStreamBinding(VisualizerMetadata visualizerMetadata) => - new StreamBinding( - this.SourceStreamMetadata.Name, - this.PartitionViewModel.Name, - this.Path, - visualizerMetadata.StreamAdapterType, - visualizerMetadata.VisualizationObjectType == typeof(LatencyVisualizationObject) || visualizerMetadata.VisualizationObjectType == typeof(MessageVisualizationObject) ? null : new object[] { this.MemberPath }, - visualizerMetadata.SummarizerType, - null, - true); - - /// - public override void EnsureDerivedStreamExists(StreamBinding streamBinding) - { - var memberPath = streamBinding.StreamAdapterArguments[0] as string; - if (memberPath.StartsWith($"{this.MemberPath}.")) - { - var remainingPath = memberPath.Substring(this.MemberPath.Length + 1); - - if (this.FindStreamTreeNode(remainingPath) == null) - { - this.AddDerivedMemberStreamChildren(); - this.ExpandAll(); - } - - if (remainingPath.Contains('.')) - { - var pathItems = remainingPath.Split('.'); - if (this.InternalChildren.FirstOrDefault(p => p.Name == pathItems.First()) is DerivedMemberStreamTreeNode memberChild) - { - memberChild.EnsureDerivedStreamExists(streamBinding); - } - } - } - } - - /// - protected override void InsertCustomAdapters(List metadatas) - { - var streamSourceDataType = VisualizationContext.Instance.GetDataType(this.SourceStreamMetadata.TypeName); - - // For each of the non-universal visualization objects, add a data adapter from the stream data type to the subfield data type - for (int index = 0; index < metadatas.Count; index++) - { - // For message visualization object insert a custom object adapter so values can be displayed for known types. - if (metadatas[index].VisualizationObjectType == typeof(MessageVisualizationObject)) - { - var objectAdapterType = typeof(ObjectAdapter<>).MakeGenericType(streamSourceDataType); - metadatas[index] = metadatas[index].GetCloneWithNewStreamAdapterType(objectAdapterType); - } - else if (metadatas[index].VisualizationObjectType == typeof(LatencyVisualizationObject)) - { - // O/w for latency visualization object insert a custom object adapter so values can be displayed for known types. - var objectToLatencyAdapterType = typeof(ObjectToLatencyAdapter<>).MakeGenericType(streamSourceDataType); - metadatas[index] = metadatas[index].GetCloneWithNewStreamAdapterType(objectToLatencyAdapterType); - } - else - { - // If the visualizer metadata already contains a stream adapter, create a stream member adapter that - // encapsulates it, otherwise create a stream member adapter that adapts directly from the message - // type to the member type. - Type streamMemberAdapterType; - if (metadatas[index].StreamAdapterType != null) - { - var existingStreamAdapter = (IStreamAdapter)Activator.CreateInstance(metadatas[index].StreamAdapterType); - streamMemberAdapterType = typeof(StreamMemberAdapter<,,,>).MakeGenericType( - streamSourceDataType, - existingStreamAdapter.SourceType, - metadatas[index].StreamAdapterType, - existingStreamAdapter.DestinationType); - } - else - { - streamMemberAdapterType = typeof(StreamMemberAdapter<,>).MakeGenericType( - streamSourceDataType, - metadatas[index].DataType); - } - - metadatas[index] = metadatas[index].GetCloneWithNewStreamAdapterType(streamMemberAdapterType); - } - } - } - - /// - protected override void AddDerivedMemberStreamChildren() - { - // Get the type of this node. - Type dataType = TypeResolutionHelper.GetVerifiedType(this.DataTypeName); - - // If this is already an auto-generated nullable, then the type we care to expand is - // the value-type inside the nullable type. - if (this.IsAutoGeneratedNullableMember) - { - dataType = dataType.GenericTypeArguments[0]; - } - - // Determine if the current node is a reference type - var isReference = this.IsAutoGeneratedNullableMember || !dataType.IsValueType || Nullable.GetUnderlyingType(dataType) != null; - - if (dataType != null) - { - // Add a child node for each public instance property that takes no parameters. - foreach (PropertyInfo propertyInfo in dataType.GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(property => !property.GetMethod.GetParameters().Any())) - { - this.AddDerivedMemberStreamChild(propertyInfo, propertyInfo.PropertyType, isReference && propertyInfo.PropertyType.IsValueType); - } - - // Add a child node for each public instance field - foreach (FieldInfo fieldInfo in dataType.GetFields(BindingFlags.Public | BindingFlags.Instance)) - { - this.AddDerivedMemberStreamChild(fieldInfo, fieldInfo.FieldType, isReference && fieldInfo.FieldType.IsValueType); - } - } - } - - /// - protected override bool CanExpandDerivedMemberStreams() - { - // If we have already expanded this node with derived member streams - if (this.InternalChildren.Any(c => c is DerivedMemberStreamTreeNode)) - { - // Then no longer expand - return false; - } - - // Get the node type - var nodeType = TypeResolutionHelper.GetVerifiedType(this.DataTypeName); - - // If it's an auto-generated nullable, we need to assess whether the inner value-type (inside the nullable) - // can expand the members. - if (this.IsAutoGeneratedNullableMember) - { - nodeType = nodeType.GenericTypeArguments[0]; - } - - if (nodeType != null) - { - return nodeType.GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(property => !property.GetMethod.GetParameters().Any()).Any() || nodeType.GetFields(BindingFlags.Public | BindingFlags.Instance).Any(); - } - - return false; - } - } -} diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/ViewModels/DerivedReceiverDiagnosticsStreamTreeNode.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/ViewModels/DerivedReceiverDiagnosticsStreamTreeNode.cs deleted file mode 100644 index 70eb9b5c9..000000000 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/ViewModels/DerivedReceiverDiagnosticsStreamTreeNode.cs +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. - -namespace Microsoft.Psi.Visualization.ViewModels -{ - using System; - using System.Collections.Generic; - using System.ComponentModel; - using Microsoft.Psi.Diagnostics; - using Microsoft.Psi.Visualization.Adapters; - using Microsoft.Psi.Visualization.Data; - using Microsoft.Psi.Visualization.VisualizationObjects; - - /// - /// Implements a node in the stream tree that holds information about a derived - /// receiver diagnostic statistic. - /// - /// The type of the diagnostic statistic. - public class DerivedReceiverDiagnosticsStreamTreeNode : DerivedStreamTreeNode - where T : struct - { - private readonly Func memberFunc; - - /// - /// Initializes a new instance of the class. - /// - /// The partition where this stream tree node can be found. - /// The path to the stream tree node. - /// The name of the stream tree node. - /// The source stream metadata. - /// The receiver id. - /// A function that given the receiver diagnostics provides the statistic of interest. - public DerivedReceiverDiagnosticsStreamTreeNode( - PartitionViewModel partitionViewModel, - string path, - string name, - IStreamMetadata sourceStreamMetadata, - int receiverId, - Func memberFunc) - : base(partitionViewModel, path, name, sourceStreamMetadata) - { - this.DataTypeName = typeof(T?).FullName; - this.ReceiverId = receiverId; - this.memberFunc = memberFunc; - } - - /// - /// Gets the receiver id. - /// - [DisplayName("Receiver Id")] - [Description("The receiver id.")] - public int ReceiverId { get; private set; } - - /// - public override StreamBinding CreateStreamBinding(VisualizerMetadata visualizerMetadata) => - new StreamBinding( - this.SourceStreamMetadata.Name, - this.PartitionViewModel.Name, - this.Path, - visualizerMetadata.StreamAdapterType, - visualizerMetadata.VisualizationObjectType == typeof(LatencyVisualizationObject) || visualizerMetadata.VisualizationObjectType == typeof(MessageVisualizationObject) ? null : new object[] { this.ReceiverId, this.memberFunc }, - null, - null, - true); - - /// - protected override void InsertCustomAdapters(List metadatas) - { - var streamSourceDataType = VisualizationContext.Instance.GetDataType(this.SourceStreamMetadata.TypeName); - - // For each of the non-universal visualization objects, add a data adapter from the stream data type to the subfield data type - for (int index = 0; index < metadatas.Count; index++) - { - // For message visualization object insert a custom object adapter so values can be displayed for known types. - if (metadatas[index].VisualizationObjectType == typeof(MessageVisualizationObject)) - { - var objectAdapterType = typeof(ObjectAdapter<>).MakeGenericType(streamSourceDataType); - metadatas[index] = metadatas[index].GetCloneWithNewStreamAdapterType(objectAdapterType); - } - else if (metadatas[index].VisualizationObjectType == typeof(LatencyVisualizationObject)) - { - // O/w for latency visualization object insert a custom object adapter so values can be displayed for known types. - var objectToLatencyAdapterType = typeof(ObjectToLatencyAdapter<>).MakeGenericType(streamSourceDataType); - metadatas[index] = metadatas[index].GetCloneWithNewStreamAdapterType(objectToLatencyAdapterType); - } - else - { - // If the visualizer metadata already contains a stream adapter, create a stream member adapter that - // encapsulates it, otherwise create a stream member adapter that adapts directly from the message - // type to the member type. - Type streamMemberAdapterType; - if (metadatas[index].StreamAdapterType != null) - { - throw new NotSupportedException("Recevider diagnostics member adapter cannot be applied in conjunction with an existing adapter."); - } - else - { - streamMemberAdapterType = typeof(PipelineDiagnosticsToReceiverDiagnosticsMemberStreamAdapter<>).MakeGenericType(metadatas[index].DataType.GetGenericArguments()[0]); - } - - metadatas[index] = metadatas[index].GetCloneWithNewStreamAdapterType(streamMemberAdapterType); - } - } - } - - /// - protected override bool CanExpandDerivedMemberStreams() => false; - } -} \ No newline at end of file diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/ViewModels/DerivedStreamContainerTreeNode.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/ViewModels/DerivedStreamContainerTreeNode.cs deleted file mode 100644 index 4374fb888..000000000 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/ViewModels/DerivedStreamContainerTreeNode.cs +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. - -namespace Microsoft.Psi.Visualization.ViewModels -{ - using System.ComponentModel; - using System.Windows.Media; - using Microsoft.Psi.Visualization; - - /// - /// Implements a node in the dataset tree that represents a derived stream container. - /// - public class DerivedStreamContainerTreeNode : StreamContainerTreeNode - { - /// - /// Initializes a new instance of the class. - /// - /// The partition for the container tree node. - /// The path to the container tree node. - /// The name of the container tree node. - public DerivedStreamContainerTreeNode(PartitionViewModel partitionViewModel, string path, string name) - : base(partitionViewModel, path, name) - { - } - - /// - public override string IconSource => this.PartitionViewModel.IsLivePartition ? IconSourcePath.GroupLive : IconSourcePath.Group; - - /// - public override Brush ForegroundBrush => new SolidColorBrush(Colors.LightGray); - - /// - [Browsable(false)] - public override long SubsumedMessageCount => 0; - - /// - [Browsable(false)] - public override long SubsumedSize => 0; - - /// - [Browsable(false)] - public override string SubsumedOpenedTimeString => string.Empty; - - /// - [Browsable(false)] - public override string SubsumedClosedTimeString => string.Empty; - - /// - [Browsable(false)] - public override string SubsumedFirstMessageOriginatingTimeString => string.Empty; - - /// - [Browsable(false)] - public override string SubsumedFirstMessageCreationTimeString => string.Empty; - - /// - [Browsable(false)] - public override string SubsumedLastMessageOriginatingTimeString => string.Empty; - - /// - [Browsable(false)] - public override string SubsumedLastMessageCreationTimeString => string.Empty; - - /// - protected override void UpdateAuxiliaryInfo() - { - this.AuxiliaryInfo = string.Empty; - } - } -} diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/ViewModels/DerivedStreamTreeNode.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/ViewModels/DerivedStreamTreeNode.cs deleted file mode 100644 index 4d4390ccb..000000000 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/ViewModels/DerivedStreamTreeNode.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. - -namespace Microsoft.Psi.Visualization.ViewModels -{ - using System.Windows.Media; - - /// - /// Implements a node in the dataset tree that represents a derived stream. - /// - /// This class acts as a base class for various types of derived stream tree - /// nodes such as or - /// . - public abstract class DerivedStreamTreeNode : StreamTreeNode - { - /// - /// Initializes a new instance of the class. - /// - /// The partition for the stream tree node. - /// The path to the stream tree node. - /// The name of the stream tree node. - /// The source stream metadata. - public DerivedStreamTreeNode(PartitionViewModel partitionViewModel, string path, string name, IStreamMetadata sourceStreamMetadata) - : base(partitionViewModel, path, name, sourceStreamMetadata) - { - } - - /// - public override string IconSource => this.PartitionViewModel.IsLivePartition ? IconSourcePath.StreamMemberLive : IconSourcePath.StreamMember; - - /// - public override Brush ForegroundBrush => new SolidColorBrush(Colors.LightGray); - - /// - protected override void UpdateAuxiliaryInfo() - { - this.AuxiliaryInfo = string.Empty; - } - } -} diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/ViewModels/PartitionViewModel.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/ViewModels/PartitionViewModel.cs index 2e90470bd..9132db459 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/ViewModels/PartitionViewModel.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/ViewModels/PartitionViewModel.cs @@ -56,7 +56,7 @@ public class PartitionViewModel : ObservableTreeNodeObject, IDisposable private string auxiliaryInfo = string.Empty; - private StreamContainerTreeNode streamTreeRoot; + private StreamTreeNode rootStreamTreeNode; private bool isDirty = false; private bool isLivePartition = false; private Thread monitorWorker = null; @@ -66,6 +66,7 @@ public class PartitionViewModel : ObservableTreeNodeObject, IDisposable private RelayCommand saveChangesCommand; private RelayCommand exportStoreCommand; private RelayCommand removePartitionCommand; + private RelayCommand removePartitionFromAllSessionsCommand; private RelayCommand contextMenuOpeningCommand; /// @@ -79,10 +80,10 @@ public PartitionViewModel(SessionViewModel sessionViewModel, IPartition partitio this.sessionViewModel = sessionViewModel; this.sessionViewModel.DatasetViewModel.PropertyChanged += this.OnDatasetViewModelPropertyChanged; this.streamsById = new Dictionary(); - this.StreamTreeRoot = new StreamContainerTreeNode(this, null, null); + this.RootStreamTreeNode = StreamTreeNode.CreateRoot(this); foreach (var stream in this.partition.AvailableStreams) { - this.streamsById[stream.Id] = this.StreamTreeRoot.AddStreamTreeNode(stream); + this.streamsById[stream.Id] = this.RootStreamTreeNode.AddChild(stream.Name, stream, null, null); this.streamsById[stream.Id].IsTreeNodeExpanded = true; } @@ -95,8 +96,6 @@ public PartitionViewModel(SessionViewModel sessionViewModel, IPartition partitio this.MonitorLivePartition(); } - this.IsTreeNodeExpanded = true; - // If there's no store backing the partition, alert the user. if (!this.partition.IsStoreValid) { @@ -119,19 +118,7 @@ public PartitionViewModel(SessionViewModel sessionViewModel, IPartition partitio [Browsable(false)] [IgnoreDataMember] public RelayCommand SaveChangesCommand - { - get - { - if (this.saveChangesCommand == null) - { - this.saveChangesCommand = new RelayCommand( - () => this.SaveChanges(), - () => this.IsDirty); - } - - return this.saveChangesCommand; - } - } + => this.saveChangesCommand ??= new RelayCommand(() => this.SaveChanges(), () => this.IsDirty); /// /// Gets the export store command. @@ -139,19 +126,9 @@ public RelayCommand SaveChangesCommand [Browsable(false)] [IgnoreDataMember] public RelayCommand ExportStoreCommand - { - get - { - if (this.exportStoreCommand == null) - { - this.exportStoreCommand = new RelayCommand( - () => this.ExportStore(), - () => this.IsPsiPartition && !this.IsLivePartition && !this.isDirty && this.Partition.OriginatingTimeInterval != TimeInterval.Empty); - } - - return this.exportStoreCommand; - } - } + => this.exportStoreCommand ??= new RelayCommand( + () => this.ExportStore(), + () => this.IsPsiPartition && !this.IsLivePartition && !this.isDirty && this.Partition.MessageOriginatingTimeInterval != TimeInterval.Empty); /// /// Gets or sets the partition name. @@ -217,26 +194,26 @@ public string AuxiliaryInfo /// [DisplayName("First Message OriginatingTime")] [Description("The originating time of the first message in the partition.")] - public string FirstMessageOriginatingTimeString => DateTimeFormatHelper.FormatDateTime(this.FirstMessageOriginatingTime); + public string FirstMessageOriginatingTimeString => DateTimeHelper.FormatDateTime(this.FirstMessageOriginatingTime); /// /// Gets a string representation of the originating time of the last message in the partition. /// [DisplayName("Last Message OriginatingTime")] [Description("The originating time of the last message in the partition.")] - public string LastMessageOriginatingTimeString => DateTimeFormatHelper.FormatDateTime(this.LastMessageOriginatingTime); + public string LastMessageOriginatingTimeString => DateTimeHelper.FormatDateTime(this.LastMessageOriginatingTime); /// /// Gets the originating time of the first message in the partition. /// [Browsable(false)] - public DateTime FirstMessageOriginatingTime => this.streamTreeRoot.SubsumedFirstMessageOriginatingTime; + public DateTime? FirstMessageOriginatingTime => this.rootStreamTreeNode.SubsumedFirstMessageOriginatingTime; /// /// Gets the originating time of the last message in the partition. /// [Browsable(false)] - public DateTime LastMessageOriginatingTime => this.streamTreeRoot.SubsumedLastMessageOriginatingTime; + public DateTime? LastMessageOriginatingTime => this.rootStreamTreeNode.SubsumedLastMessageOriginatingTime; /// /// Gets the dataset that this partition belongs to. @@ -274,10 +251,10 @@ private set /// Gets or sets the root stream tree node of this partition. /// [Browsable(false)] - public StreamContainerTreeNode StreamTreeRoot + public StreamTreeNode RootStreamTreeNode { - get => this.streamTreeRoot; - set => this.Set(nameof(this.StreamTreeRoot), ref this.streamTreeRoot, value); + get => this.rootStreamTreeNode; + set => this.Set(nameof(this.RootStreamTreeNode), ref this.rootStreamTreeNode, value); } /// @@ -345,6 +322,14 @@ public string IconSource [IgnoreDataMember] public RelayCommand RemovePartitionCommand => this.removePartitionCommand ??= new RelayCommand(() => this.RemovePartition()); + /// + /// Gets the command for removing this partition from all sessions. + /// + [Browsable(false)] + [IgnoreDataMember] + public RelayCommand RemovePartitionFromAllSessionsCommand => + this.removePartitionFromAllSessionsCommand ??= new RelayCommand(() => this.DatasetViewModel.RemovePartitionFromAllSessions(this.Name)); + /// /// Gets the command that executes when opening the partition context menu. /// @@ -364,10 +349,10 @@ public void Update(IPartition partition) { this.partition = partition; this.streamsById.Clear(); - this.StreamTreeRoot = new StreamContainerTreeNode(this, null, null); + this.RootStreamTreeNode = StreamTreeNode.CreateRoot(this); foreach (var stream in this.partition.AvailableStreams) { - this.streamsById[stream.Id] = this.StreamTreeRoot.AddStreamTreeNode(stream); + this.streamsById[stream.Id] = this.RootStreamTreeNode.AddChild(stream.Name, stream, null, null); } // Check if this is a live partition (i.e. it still has a writer attached) @@ -394,7 +379,7 @@ public StreamSource CreateStreamSource(StreamBinding streamBinding, Func s.Name == streamBinding.StreamName); + var streamMetadata = this.Partition.AvailableStreams.FirstOrDefault(s => s.Name == streamBinding.SourceStreamName); if (streamMetadata != default) { if (streamBinding.Summarizer != null) @@ -412,7 +397,7 @@ public StreamSource CreateStreamSource(StreamBinding streamBinding, Func /// The full name of the node to select. - /// True if the stream was found and selected, otherwise false. - public bool SelectNode(string nodeName) + /// True if the node was found and selected, otherwise false. + public bool SelectStreamTreeNode(string nodeName) { - if (this.StreamTreeRoot != null) + if (this.RootStreamTreeNode != null) { - if (this.StreamTreeRoot.SelectTreeNode(nodeName)) + if (this.RootStreamTreeNode.SelectChild(nodeName)) { this.SessionViewModel.IsTreeNodeExpanded = true; this.IsTreeNodeExpanded = true; @@ -450,12 +435,12 @@ public bool SelectNode(string nodeName) } /// - /// Finds a stream tree node within a partition. + /// Finds a node in the partition. /// - /// The name of the stream to search for. - /// A stream tree node representing the stream, or null if the stream does not exist in the partition. - public StreamTreeNode FindStreamTreeNode(string streamName) => - this.StreamTreeRoot?.FindStreamTreeNode(streamName); + /// The full name of the node to find. + /// The found stream tree node, or null if the node does not exist in the partition. + public StreamTreeNode FindStreamTreeNode(string nodeName) => + this.RootStreamTreeNode?.FindChild(nodeName); /// /// Saves all uncommitted changes of all streams in the partition to the store. @@ -475,7 +460,11 @@ public void SaveChanges() } }); - Task.Run(() => DataManager.Instance.SaveStore(this.StoreName, this.StorePath, progress)); + Task.Run(() => + { + DataManager.Instance.SaveStore(this.StoreName, this.StorePath, progress); + Console.WriteLine(); + }); // Show the modal progress window. If the task has already completed then it will have // closed the progress window and an invalid operation exception will be thrown. @@ -540,7 +529,7 @@ public void ChangeStoreStatus(bool isDirty, string[] streamNames) // Update the dirty flag on all of the streams that had changes saved. foreach (string streamName in streamNames) { - var streamTreeNode = this.StreamTreeRoot.FindStreamTreeNode(streamName); + var streamTreeNode = this.RootStreamTreeNode.FindChild(streamName); if (streamTreeNode != null) { streamTreeNode.IsDirty = isDirty; @@ -579,7 +568,7 @@ internal bool UpdateLiveStatus(bool initialCheck = false) private async void ExportStore() { // Set the initial crop interval to be the same as the partition's originating time interval - TimeInterval partitionInterval = this.Partition.OriginatingTimeInterval; + var partitionInterval = this.Partition.TimeInterval; DateTime cropIntervalLeft = partitionInterval.Left; DateTime cropIntervalRight = partitionInterval.Right; @@ -617,21 +606,21 @@ private async void ExportStore() TimeInterval requestedCropInterval = dlg.CropInterval; // Make sure the requested crop interval does not fall outside the partition interval - if (requestedCropInterval.Left < this.Partition.OriginatingTimeInterval.Left) + if (requestedCropInterval.Left < partitionInterval.Left) { - requestedCropInterval = new TimeInterval(this.Partition.OriginatingTimeInterval.Left, requestedCropInterval.Right); + requestedCropInterval = new TimeInterval(partitionInterval.Left, requestedCropInterval.Right); } - if (requestedCropInterval.Right > this.Partition.OriginatingTimeInterval.Right) + if (requestedCropInterval.Right > partitionInterval.Right) { - requestedCropInterval = new TimeInterval(requestedCropInterval.Left, this.Partition.OriginatingTimeInterval.Right); + requestedCropInterval = new TimeInterval(requestedCropInterval.Left, partitionInterval.Right); } // Export the store Task exportTask = Task.Run(() => PsiStore.Crop( (this.StoreName, this.StorePath), (dlg.StoreName, dlg.StorePath), - requestedCropInterval.Left - this.Partition.OriginatingTimeInterval.Left, + requestedCropInterval.Left - partitionInterval.Left, RelativeTimeInterval.Future(requestedCropInterval.Span), false, progress)); @@ -729,7 +718,7 @@ private void OnMessageWritten(Envelope envelope) } } - private void OnMetadataUpdate(IEnumerable metadata, RuntimeInfo runtimeVersion) + private void OnMetadataUpdate(IEnumerable metadata, RuntimeInfo runtimeInfo) { // Switch to the main UI thread for handling this message Application.Current?.Dispatcher.Invoke(this.newMetadataCallback, metadata); @@ -747,7 +736,7 @@ private void UpdateStreamMetadata(IEnumerable metadata) if (!this.streamsById.ContainsKey(psiStreamMetadata.Id)) { IStreamMetadata streamMetadata = new PsiLiveStreamMetadata(psiStreamMetadata.Name, psiStreamMetadata.Id, psiStreamMetadata.TypeName, psiStreamMetadata.SupplementalMetadataTypeName, this.StoreName, this.StorePath); - this.streamsById[streamMetadata.Id] = this.StreamTreeRoot.AddStreamTreeNode(streamMetadata); + this.streamsById[streamMetadata.Id] = this.RootStreamTreeNode.AddChild(streamMetadata.Name, streamMetadata, null, null); } } } @@ -773,42 +762,42 @@ private void UpdateAuxiliaryInfo() this.AuxiliaryInfo = string.Empty; break; case AuxiliaryPartitionInfo.Duration: - this.AuxiliaryInfo = this.Partition.OriginatingTimeInterval.IsEmpty ? "?" : this.Partition.OriginatingTimeInterval.Span.ToString(@"d\.hh\:mm\:ss"); + this.AuxiliaryInfo = this.Partition.TimeInterval.IsEmpty ? "?" : this.Partition.TimeInterval.Span.ToString(@"d\.hh\:mm\:ss"); break; case AuxiliaryPartitionInfo.StartDate: - this.AuxiliaryInfo = this.Partition.OriginatingTimeInterval.IsEmpty ? "?" : this.Partition.OriginatingTimeInterval.Left.ToShortDateString(); + this.AuxiliaryInfo = this.Partition.TimeInterval.IsEmpty ? "?" : this.Partition.TimeInterval.Left.ToShortDateString(); break; case AuxiliaryPartitionInfo.StartDateLocal: - this.AuxiliaryInfo = this.Partition.OriginatingTimeInterval.IsEmpty ? "?" : this.Partition.OriginatingTimeInterval.Left.ToLocalTime().ToShortDateString(); + this.AuxiliaryInfo = this.Partition.TimeInterval.IsEmpty ? "?" : this.Partition.TimeInterval.Left.ToLocalTime().ToShortDateString(); break; case AuxiliaryPartitionInfo.StartTime: - this.AuxiliaryInfo = this.Partition.OriginatingTimeInterval.IsEmpty ? "?" : this.Partition.OriginatingTimeInterval.Left.ToShortTimeString(); + this.AuxiliaryInfo = this.Partition.TimeInterval.IsEmpty ? "?" : this.Partition.TimeInterval.Left.ToShortTimeString(); break; case AuxiliaryPartitionInfo.StartTimeLocal: - this.AuxiliaryInfo = this.Partition.OriginatingTimeInterval.IsEmpty ? "?" : this.Partition.OriginatingTimeInterval.Left.ToLocalTime().ToShortTimeString(); + this.AuxiliaryInfo = this.Partition.TimeInterval.IsEmpty ? "?" : this.Partition.TimeInterval.Left.ToLocalTime().ToShortTimeString(); break; case AuxiliaryPartitionInfo.StartDateTime: - this.AuxiliaryInfo = this.Partition.OriginatingTimeInterval.IsEmpty ? "?" : this.Partition.OriginatingTimeInterval.Left.ToString(); + this.AuxiliaryInfo = this.Partition.TimeInterval.IsEmpty ? "?" : this.Partition.TimeInterval.Left.ToString(); break; case AuxiliaryPartitionInfo.StartDateTimeLocal: - this.AuxiliaryInfo = this.Partition.OriginatingTimeInterval.IsEmpty ? "?" : this.Partition.OriginatingTimeInterval.Left.ToLocalTime().ToString(); + this.AuxiliaryInfo = this.Partition.TimeInterval.IsEmpty ? "?" : this.Partition.TimeInterval.Left.ToLocalTime().ToString(); break; case AuxiliaryPartitionInfo.Size: - this.AuxiliaryInfo = this.Partition.Size.HasValue ? SizeFormatHelper.FormatSize(this.Partition.Size.Value) : "?"; + this.AuxiliaryInfo = this.Partition.Size.HasValue ? SizeHelper.FormatSize(this.Partition.Size.Value) : "?"; break; case AuxiliaryPartitionInfo.DataThroughputPerHour: - this.AuxiliaryInfo = this.Partition.Size.HasValue && !this.Partition.OriginatingTimeInterval.IsEmpty ? - SizeFormatHelper.FormatThroughput(this.Partition.Size.Value / this.Partition.OriginatingTimeInterval.Span.TotalHours, "hour") : + this.AuxiliaryInfo = this.Partition.Size.HasValue && !this.Partition.TimeInterval.IsEmpty ? + SizeHelper.FormatThroughput(this.Partition.Size.Value / this.Partition.TimeInterval.Span.TotalHours, "hour") : "?"; break; case AuxiliaryPartitionInfo.DataThroughputPerMinute: - this.AuxiliaryInfo = this.Partition.Size.HasValue && !this.Partition.OriginatingTimeInterval.IsEmpty ? - SizeFormatHelper.FormatThroughput(this.Partition.Size.Value / this.Partition.OriginatingTimeInterval.Span.TotalMinutes, "min") : + this.AuxiliaryInfo = this.Partition.Size.HasValue && !this.Partition.TimeInterval.IsEmpty ? + SizeHelper.FormatThroughput(this.Partition.Size.Value / this.Partition.TimeInterval.Span.TotalMinutes, "min") : "?"; break; case AuxiliaryPartitionInfo.DataThroughputPerSecond: - this.AuxiliaryInfo = this.Partition.Size.HasValue && !this.Partition.OriginatingTimeInterval.IsEmpty ? - SizeFormatHelper.FormatThroughput(this.Partition.Size.Value / this.Partition.OriginatingTimeInterval.Span.TotalSeconds, "sec") : + this.AuxiliaryInfo = this.Partition.Size.HasValue && !this.Partition.TimeInterval.IsEmpty ? + SizeHelper.FormatThroughput(this.Partition.Size.Value / this.Partition.TimeInterval.Span.TotalSeconds, "sec") : "?"; break; case AuxiliaryPartitionInfo.StreamCount: @@ -852,6 +841,7 @@ private ContextMenu CreateContextMenu() contextMenu.Items.Add(MenuItemHelper.CreateMenuItem(IconSourcePath.PartitionExport, "Export Partition", this.ExportStoreCommand)); contextMenu.Items.Add(MenuItemHelper.CreateMenuItem(IconSourcePath.PartitionAdd, "Save Changes", this.SaveChangesCommand)); contextMenu.Items.Add(MenuItemHelper.CreateMenuItem(IconSourcePath.PartitionRemove, "Remove", this.RemovePartitionCommand)); + contextMenu.Items.Add(MenuItemHelper.CreateMenuItem(null, "Remove From All Sessions", this.RemovePartitionFromAllSessionsCommand)); contextMenu.Items.Add(new Separator()); // Add the visualize session context menu if the partition is not in the currently visualized session @@ -861,6 +851,51 @@ private ContextMenu CreateContextMenu() contextMenu.Items.Add(new Separator()); } + // Add copy to clipboard menu with sub-menu items + var copyToClipboardMenuItem = MenuItemHelper.CreateMenuItem( + string.Empty, + "Copy to Clipboard", + null); + + copyToClipboardMenuItem.Items.Add( + MenuItemHelper.CreateMenuItem( + null, + "Session Name", + VisualizationContext.Instance.VisualizationContainer.Navigator.CopyToClipboardCommand, + null, + true, + this.SessionViewModel.Name)); + + copyToClipboardMenuItem.Items.Add( + MenuItemHelper.CreateMenuItem( + null, + "Partition Name", + VisualizationContext.Instance.VisualizationContainer.Navigator.CopyToClipboardCommand, + null, + true, + this.Name)); + + copyToClipboardMenuItem.Items.Add( + MenuItemHelper.CreateMenuItem( + null, + "Partition Store Name", + VisualizationContext.Instance.VisualizationContainer.Navigator.CopyToClipboardCommand, + null, + true, + this.StoreName)); + + copyToClipboardMenuItem.Items.Add( + MenuItemHelper.CreateMenuItem( + null, + "Partition Store Path", + VisualizationContext.Instance.VisualizationContainer.Navigator.CopyToClipboardCommand, + null, + true, + this.StorePath)); + + contextMenu.Items.Add(copyToClipboardMenuItem); + contextMenu.Items.Add(new Separator()); + // Add show partition info menu var showPartitionInfoMenuItem = MenuItemHelper.CreateMenuItem(string.Empty, "Show Partitions Info", null); foreach (var auxiliaryPartitionInfo in Enum.GetValues(typeof(AuxiliaryPartitionInfo))) diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/ViewModels/PipelineDiagnosticsStreamTreeNode.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/ViewModels/PipelineDiagnosticsStreamTreeNode.cs deleted file mode 100644 index 7c13f5092..000000000 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/ViewModels/PipelineDiagnosticsStreamTreeNode.cs +++ /dev/null @@ -1,242 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. - -namespace Microsoft.Psi.Visualization.ViewModels -{ - using System.ComponentModel; - using System.Linq; - using Microsoft.Psi.Diagnostics; - using Microsoft.Psi.Visualization.Data; - - /// - /// Implements a stream tree node for pipeline diagnostics streams. - /// - public class PipelineDiagnosticsStreamTreeNode : StreamTreeNode - { - /// - /// Initializes a new instance of the class. - /// - /// The partition for the stream tree node. - /// The path to the stream tree node. - /// The name of the stream tree node. - /// The stream metadata. - public PipelineDiagnosticsStreamTreeNode(PartitionViewModel partitionViewModel, string path, string name, IStreamMetadata streamMetadata) - : base(partitionViewModel, path, name, streamMetadata) - { - } - - /// - /// Gets the path to the stream's icon. - /// - [Browsable(false)] - public override string IconSource => this.PartitionViewModel.IsLivePartition ? IconSourcePath.DiagnosticsLive : IconSourcePath.Diagnostics; - - /// - /// Creates a set of receiver diagnostics children corresponding to a given receiver id. - /// - /// The receiver id. - /// The stream container tree node for those receiver diagnostics streams. - public DerivedStreamContainerTreeNode AddDerivedReceiverDiagnosticsChildren(int receiverId) - { - var receiverDiagnostics = this.InternalChildren.FirstOrDefault(c => c.Name == "ReceiverDiagnostics"); - if (receiverDiagnostics == null) - { - receiverDiagnostics = new DerivedStreamContainerTreeNode(this.PartitionViewModel, $"{this.Path}.ReceiverDiagnostics", "ReceiverDiagnostics"); - - // Insert the child into the existing list, before all non-member sub-streams, and in alphabetical order - var lastOrDefault = this.InternalChildren.LastOrDefault(stn => string.Compare(stn.Name, "0") < 0 && stn is StreamTreeNode); - var index = lastOrDefault != null ? this.InternalChildren.IndexOf(lastOrDefault) + 1 : 0; - this.InternalChildren.Insert(index, receiverDiagnostics); - } - - if (receiverDiagnostics.Children.FirstOrDefault(c => c.Name == $"{receiverId}") is not DerivedStreamContainerTreeNode receiverContainer) - { - receiverContainer = new DerivedStreamContainerTreeNode(this.PartitionViewModel, $"{this.Path}.ReceiverDiagnostics.{receiverId}", $"{receiverId}"); - receiverDiagnostics.AddChildTreeNode(receiverContainer); - - receiverContainer.AddChildTreeNode( - new DerivedReceiverDiagnosticsStreamTreeNode( - this.PartitionViewModel, - $"{this.Path}.ReceiverDiagnostics.{receiverId}.{nameof(PipelineDiagnostics.ReceiverDiagnostics.AvgMessageEmittedLatency)}", - nameof(PipelineDiagnostics.ReceiverDiagnostics.AvgMessageEmittedLatency), - this.SourceStreamMetadata, - receiverId, - pd => pd.AvgMessageEmittedLatency)); - - receiverContainer.AddChildTreeNode( - new DerivedReceiverDiagnosticsStreamTreeNode( - this.PartitionViewModel, - $"{this.Path}.ReceiverDiagnostics.{receiverId}.{nameof(PipelineDiagnostics.ReceiverDiagnostics.AvgMessageCreatedLatency)}", - nameof(PipelineDiagnostics.ReceiverDiagnostics.AvgMessageCreatedLatency), - this.SourceStreamMetadata, - receiverId, - pd => pd.AvgMessageCreatedLatency)); - - receiverContainer.AddChildTreeNode( - new DerivedReceiverDiagnosticsStreamTreeNode( - this.PartitionViewModel, - $"{this.Path}.ReceiverDiagnostics.{receiverId}.{nameof(PipelineDiagnostics.ReceiverDiagnostics.AvgMessageProcessTime)}", - nameof(PipelineDiagnostics.ReceiverDiagnostics.AvgMessageProcessTime), - this.SourceStreamMetadata, - receiverId, - pd => pd.AvgMessageProcessTime)); - - receiverContainer.AddChildTreeNode( - new DerivedReceiverDiagnosticsStreamTreeNode( - this.PartitionViewModel, - $"{this.Path}.ReceiverDiagnostics.{receiverId}.{nameof(PipelineDiagnostics.ReceiverDiagnostics.AvgMessageReceivedLatency)}", - nameof(PipelineDiagnostics.ReceiverDiagnostics.AvgMessageReceivedLatency), - this.SourceStreamMetadata, - receiverId, - pd => pd.AvgMessageReceivedLatency)); - - receiverContainer.AddChildTreeNode( - new DerivedReceiverDiagnosticsStreamTreeNode( - this.PartitionViewModel, - $"{this.Path}.ReceiverDiagnostics.{receiverId}.{nameof(PipelineDiagnostics.ReceiverDiagnostics.AvgMessageSize)}", - nameof(PipelineDiagnostics.ReceiverDiagnostics.AvgMessageSize), - this.SourceStreamMetadata, - receiverId, - pd => pd.AvgMessageSize)); - - receiverContainer.AddChildTreeNode( - new DerivedReceiverDiagnosticsStreamTreeNode( - this.PartitionViewModel, - $"{this.Path}.ReceiverDiagnostics.{receiverId}.{nameof(PipelineDiagnostics.ReceiverDiagnostics.AvgDeliveryQueueSize)}", - nameof(PipelineDiagnostics.ReceiverDiagnostics.AvgDeliveryQueueSize), - this.SourceStreamMetadata, - receiverId, - pd => pd.AvgDeliveryQueueSize)); - - receiverContainer.AddChildTreeNode( - new DerivedReceiverDiagnosticsStreamTreeNode( - this.PartitionViewModel, - $"{this.Path}.ReceiverDiagnostics.{receiverId}.{nameof(PipelineDiagnostics.ReceiverDiagnostics.LastMessageEmittedLatency)}", - nameof(PipelineDiagnostics.ReceiverDiagnostics.LastMessageEmittedLatency), - this.SourceStreamMetadata, - receiverId, - pd => pd.LastMessageEmittedLatency)); - - receiverContainer.AddChildTreeNode( - new DerivedReceiverDiagnosticsStreamTreeNode( - this.PartitionViewModel, - $"{this.Path}.ReceiverDiagnostics.{receiverId}.{nameof(PipelineDiagnostics.ReceiverDiagnostics.LastMessageCreatedLatency)}", - nameof(PipelineDiagnostics.ReceiverDiagnostics.LastMessageCreatedLatency), - this.SourceStreamMetadata, - receiverId, - pd => pd.LastMessageCreatedLatency)); - - receiverContainer.AddChildTreeNode( - new DerivedReceiverDiagnosticsStreamTreeNode( - this.PartitionViewModel, - $"{this.Path}.ReceiverDiagnostics.{receiverId}.{nameof(PipelineDiagnostics.ReceiverDiagnostics.LastMessageProcessTime)}", - nameof(PipelineDiagnostics.ReceiverDiagnostics.LastMessageProcessTime), - this.SourceStreamMetadata, - receiverId, - pd => pd.LastMessageProcessTime)); - - receiverContainer.AddChildTreeNode( - new DerivedReceiverDiagnosticsStreamTreeNode( - this.PartitionViewModel, - $"{this.Path}.ReceiverDiagnostics.{receiverId}.{nameof(PipelineDiagnostics.ReceiverDiagnostics.LastMessageReceivedLatency)}", - nameof(PipelineDiagnostics.ReceiverDiagnostics.LastMessageReceivedLatency), - this.SourceStreamMetadata, - receiverId, - pd => pd.LastMessageReceivedLatency)); - - receiverContainer.AddChildTreeNode( - new DerivedReceiverDiagnosticsStreamTreeNode( - this.PartitionViewModel, - $"{this.Path}.ReceiverDiagnostics.{receiverId}.{nameof(PipelineDiagnostics.ReceiverDiagnostics.LastMessageSize)}", - nameof(PipelineDiagnostics.ReceiverDiagnostics.LastMessageSize), - this.SourceStreamMetadata, - receiverId, - pd => pd.LastMessageSize)); - - receiverContainer.AddChildTreeNode( - new DerivedReceiverDiagnosticsStreamTreeNode( - this.PartitionViewModel, - $"{this.Path}.ReceiverDiagnostics.{receiverId}.{nameof(PipelineDiagnostics.ReceiverDiagnostics.LastDeliveryQueueSize)}", - nameof(PipelineDiagnostics.ReceiverDiagnostics.LastDeliveryQueueSize), - this.SourceStreamMetadata, - receiverId, - pd => pd.LastDeliveryQueueSize)); - - receiverContainer.AddChildTreeNode( - new DerivedReceiverDiagnosticsStreamTreeNode( - this.PartitionViewModel, - $"{this.Path}.ReceiverDiagnostics.{receiverId}.{nameof(PipelineDiagnostics.ReceiverDiagnostics.ReceiverIsThrottled)}", - nameof(PipelineDiagnostics.ReceiverDiagnostics.ReceiverIsThrottled), - this.SourceStreamMetadata, - receiverId, - pd => pd.ReceiverIsThrottled)); - - receiverContainer.AddChildTreeNode( - new DerivedReceiverDiagnosticsStreamTreeNode( - this.PartitionViewModel, - $"{this.Path}.ReceiverDiagnostics.{receiverId}.{nameof(PipelineDiagnostics.ReceiverDiagnostics.TotalMessageDroppedCount)}", - nameof(PipelineDiagnostics.ReceiverDiagnostics.TotalMessageDroppedCount), - this.SourceStreamMetadata, - receiverId, - pd => pd.TotalMessageDroppedCount)); - - receiverContainer.AddChildTreeNode( - new DerivedReceiverDiagnosticsStreamTreeNode( - this.PartitionViewModel, - $"{this.Path}.ReceiverDiagnostics.{receiverId}.{nameof(PipelineDiagnostics.ReceiverDiagnostics.TotalMessageEmittedCount)}", - nameof(PipelineDiagnostics.ReceiverDiagnostics.TotalMessageEmittedCount), - this.SourceStreamMetadata, - receiverId, - pd => pd.TotalMessageEmittedCount)); - - receiverContainer.AddChildTreeNode( - new DerivedReceiverDiagnosticsStreamTreeNode( - this.PartitionViewModel, - $"{this.Path}.ReceiverDiagnostics.{receiverId}.{nameof(PipelineDiagnostics.ReceiverDiagnostics.TotalMessageProcessedCount)}", - nameof(PipelineDiagnostics.ReceiverDiagnostics.TotalMessageProcessedCount), - this.SourceStreamMetadata, - receiverId, - pd => pd.TotalMessageProcessedCount)); - - receiverContainer.AddChildTreeNode( - new DerivedReceiverDiagnosticsStreamTreeNode( - this.PartitionViewModel, - $"{this.Path}.ReceiverDiagnostics.{receiverId}.{nameof(PipelineDiagnostics.ReceiverDiagnostics.WindowMessageDroppedCount)}", - nameof(PipelineDiagnostics.ReceiverDiagnostics.WindowMessageDroppedCount), - this.SourceStreamMetadata, - receiverId, - pd => pd.WindowMessageDroppedCount)); - - receiverContainer.AddChildTreeNode( - new DerivedReceiverDiagnosticsStreamTreeNode( - this.PartitionViewModel, - $"{this.Path}.ReceiverDiagnostics.{receiverId}.{nameof(PipelineDiagnostics.ReceiverDiagnostics.WindowMessageEmittedCount)}", - nameof(PipelineDiagnostics.ReceiverDiagnostics.WindowMessageEmittedCount), - this.SourceStreamMetadata, - receiverId, - pd => pd.WindowMessageEmittedCount)); - - receiverContainer.AddChildTreeNode( - new DerivedReceiverDiagnosticsStreamTreeNode( - this.PartitionViewModel, - $"{this.Path}.ReceiverDiagnostics.{receiverId}.{nameof(PipelineDiagnostics.ReceiverDiagnostics.WindowMessageProcessedCount)}", - nameof(PipelineDiagnostics.ReceiverDiagnostics.WindowMessageProcessedCount), - this.SourceStreamMetadata, - receiverId, - pd => pd.WindowMessageProcessedCount)); - } - - return receiverContainer; - } - - /// - public override void EnsureDerivedStreamExists(StreamBinding streamBinding) - { - var receiverId = (int)streamBinding.StreamAdapterArguments[0]; - this.AddDerivedReceiverDiagnosticsChildren(receiverId); - } - - /// - protected override bool CanExpandDerivedMemberStreams() => false; - } -} \ No newline at end of file diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/ViewModels/SessionViewModel.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/ViewModels/SessionViewModel.cs index 5ef4a0105..981223713 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/ViewModels/SessionViewModel.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/ViewModels/SessionViewModel.cs @@ -21,6 +21,7 @@ namespace Microsoft.Psi.Visualization.ViewModels using Microsoft.Psi.Visualization.Base; using Microsoft.Psi.Visualization.Data; using Microsoft.Psi.Visualization.Helpers; + using Microsoft.Psi.Visualization.VisualizationObjects; using Microsoft.Psi.Visualization.Windows; /// @@ -103,27 +104,27 @@ public string AuxiliaryInfo /// [DisplayName("First Message OriginatingTime")] [Description("The originating time of the first message in the session.")] - public string FirstMessageOriginatingTimeString => DateTimeFormatHelper.FormatDateTime(this.FirstMessageOriginatingTime); + public string FirstMessageOriginatingTimeString => DateTimeHelper.FormatDateTime(this.FirstMessageOriginatingTime); /// /// Gets a string representation of the originating time of the last message in the session. /// [DisplayName("Last Message OriginatingTime")] [Description("The originating time of the last message in the session.")] - public string LastMessageOriginatingTimeString => DateTimeFormatHelper.FormatDateTime(this.LastMessageOriginatingTime); + public string LastMessageOriginatingTimeString => DateTimeHelper.FormatDateTime(this.LastMessageOriginatingTime); /// /// Gets the originating time of the first message in the session. /// [Browsable(false)] - public DateTime FirstMessageOriginatingTime + public DateTime? FirstMessageOriginatingTime => this.partitionViewModels.Count > 0 ? this.partitionViewModels.Min(p => p.FirstMessageOriginatingTime) : default; /// /// Gets the originating time of the last message in the session. /// [Browsable(false)] - public DateTime LastMessageOriginatingTime + public DateTime? LastMessageOriginatingTime => this.partitionViewModels.Count > 0 ? this.partitionViewModels.Max(p => p.LastMessageOriginatingTime) : default; /// @@ -139,8 +140,8 @@ public DateTime LastMessageOriginatingTime public TimeInterval OriginatingTimeInterval => TimeInterval.Coverage( this.partitionViewModels - .Where(p => p.FirstMessageOriginatingTime > DateTime.MinValue && p.LastMessageOriginatingTime < DateTime.MaxValue) - .Select(p => new TimeInterval(p.FirstMessageOriginatingTime, p.LastMessageOriginatingTime))); + .Where(p => p.FirstMessageOriginatingTime != null && p.LastMessageOriginatingTime != null) + .Select(p => new TimeInterval(p.FirstMessageOriginatingTime.Value, p.LastMessageOriginatingTime.Value))); /// /// Gets the collection of partitions in this session. @@ -436,6 +437,48 @@ public void RemoveSession() .FirstOrDefault(p => p.Name == partitionName)? .FindStreamTreeNode(streamName); + /// + /// Ensures that the derived stream tree nodes necessary for the set of visualization objects in a specified container. + /// + /// The visualization container. + public void EnsureDerivedStreamTreeNodesExist(VisualizationContainer visualizationContainer) + { + // Check if the visualization container contains any stream member visualizers. + var derivedStreamVisualizationObjects = visualizationContainer.GetDerivedStreamVisualizationObjects(); + + foreach (IStreamVisualizationObject derivedStreamVisualizationObject in derivedStreamVisualizationObjects) + { + // Find the stream tree node corresponding to the source data + var sourceStreamTreeNode = this.FindStreamTreeNode( + derivedStreamVisualizationObject.StreamBinding.PartitionName, + derivedStreamVisualizationObject.StreamBinding.SourceStreamName); + + // If the source exists + if (sourceStreamTreeNode != null) + { + // Find the derived stream tree node used by this visualizer + var streamTreeNode = this.FindStreamTreeNode( + derivedStreamVisualizationObject.StreamBinding.PartitionName, + derivedStreamVisualizationObject.StreamBinding.StreamName); + + // Find the partition view model + var partitionViewModel = this.PartitionViewModels.FirstOrDefault( + p => p.Name == derivedStreamVisualizationObject.StreamBinding.PartitionName); + + // If the derived stream does not exist, and we have the partition + if (streamTreeNode == null && partitionViewModel != null) + { + partitionViewModel.RootStreamTreeNode.AddChild( + derivedStreamVisualizationObject.StreamBinding.StreamName, + sourceStreamTreeNode.SourceStreamMetadata, + derivedStreamVisualizationObject.StreamBinding.DerivedStreamAdapterType, + derivedStreamVisualizationObject.StreamBinding.DerivedStreamAdapterArguments, + true); + } + } + } + } + /// public override string ToString() => $"Session: {this.Name}"; @@ -489,8 +532,18 @@ private void OnMouseDoubleClick(MouseButtonEventArgs e) { if (this.DatasetViewModel.CurrentSessionViewModel != this) { + // Visualize the current session this.DatasetViewModel.VisualizeSession(this); + + // Expand the session tree node to show partitions this.IsTreeNodeExpanded = true; + + // If a single partition, expand the partition to show streams + if (this.partitionViewModels.Count == 1) + { + this.partitionViewModels.First().IsTreeNodeExpanded = true; + } + e.Handled = true; } } @@ -503,37 +556,37 @@ private void UpdateAuxiliaryInfo() this.AuxiliaryInfo = string.Empty; break; case AuxiliarySessionInfo.Duration: - this.AuxiliaryInfo = this.Session.OriginatingTimeInterval.Span.ToString(@"d\.hh\:mm\:ss"); + this.AuxiliaryInfo = this.Session.TimeInterval.Span.ToString(@"d\.hh\:mm\:ss"); break; case AuxiliarySessionInfo.StartDate: - this.AuxiliaryInfo = this.Session.OriginatingTimeInterval.Left.ToShortDateString(); + this.AuxiliaryInfo = this.Session.TimeInterval.Left.ToShortDateString(); break; case AuxiliarySessionInfo.StartDateLocal: - this.AuxiliaryInfo = this.Session.OriginatingTimeInterval.Left.ToLocalTime().ToShortDateString(); + this.AuxiliaryInfo = this.Session.TimeInterval.Left.ToLocalTime().ToShortDateString(); break; case AuxiliarySessionInfo.StartTime: - this.AuxiliaryInfo = this.Session.OriginatingTimeInterval.Left.ToShortTimeString(); + this.AuxiliaryInfo = this.Session.TimeInterval.Left.ToShortTimeString(); break; case AuxiliarySessionInfo.StartTimeLocal: - this.AuxiliaryInfo = this.Session.OriginatingTimeInterval.Left.ToLocalTime().ToShortTimeString(); + this.AuxiliaryInfo = this.Session.TimeInterval.Left.ToLocalTime().ToShortTimeString(); break; case AuxiliarySessionInfo.StartDateTime: - this.AuxiliaryInfo = this.Session.OriginatingTimeInterval.Left.ToString(); + this.AuxiliaryInfo = this.Session.TimeInterval.Left.ToString(); break; case AuxiliarySessionInfo.StartDateTimeLocal: - this.AuxiliaryInfo = this.Session.OriginatingTimeInterval.Left.ToLocalTime().ToString(); + this.AuxiliaryInfo = this.Session.TimeInterval.Left.ToLocalTime().ToString(); break; case AuxiliarySessionInfo.Size: - this.AuxiliaryInfo = this.Session.Size.HasValue ? SizeFormatHelper.FormatSize(this.Session.Size.Value) : "?"; + this.AuxiliaryInfo = this.Session.Size.HasValue ? SizeHelper.FormatSize(this.Session.Size.Value) : "?"; break; case AuxiliarySessionInfo.DataThroughputPerHour: - this.AuxiliaryInfo = this.Session.Size.HasValue ? SizeFormatHelper.FormatThroughput(this.Session.Size.Value / this.Session.OriginatingTimeInterval.Span.TotalHours, "hour") : "?"; + this.AuxiliaryInfo = this.Session.Size.HasValue ? SizeHelper.FormatThroughput(this.Session.Size.Value / this.Session.TimeInterval.Span.TotalHours, "hour") : "?"; break; case AuxiliarySessionInfo.DataThroughputPerMinute: - this.AuxiliaryInfo = this.Session.Size.HasValue ? SizeFormatHelper.FormatThroughput(this.Session.Size.Value / this.Session.OriginatingTimeInterval.Span.TotalMinutes, "min") : "?"; + this.AuxiliaryInfo = this.Session.Size.HasValue ? SizeHelper.FormatThroughput(this.Session.Size.Value / this.Session.TimeInterval.Span.TotalMinutes, "min") : "?"; break; case AuxiliarySessionInfo.DataThroughputPerSecond: - this.AuxiliaryInfo = this.Session.Size.HasValue ? SizeFormatHelper.FormatThroughput(this.Session.Size.Value / this.Session.OriginatingTimeInterval.Span.TotalSeconds, "sec") : "?"; + this.AuxiliaryInfo = this.Session.Size.HasValue ? SizeHelper.FormatThroughput(this.Session.Size.Value / this.Session.TimeInterval.Span.TotalSeconds, "sec") : "?"; break; case AuxiliarySessionInfo.StreamCount: this.AuxiliaryInfo = this.Session.StreamCount.HasValue ? (this.Session.StreamCount == 0 ? "0" : $"{this.Session.StreamCount.Value:0,0}") : "?"; @@ -577,7 +630,24 @@ private ContextMenu CreateContextMenu() } contextMenu.Items.Add(runTasksMenuItem); + contextMenu.Items.Add(new Separator()); + // Add copy to clipboard menu with sub-menu items + var copyToClipboardMenuItem = MenuItemHelper.CreateMenuItem( + string.Empty, + "Copy to Clipboard", + null); + + copyToClipboardMenuItem.Items.Add( + MenuItemHelper.CreateMenuItem( + null, + "Session Name", + VisualizationContext.Instance.VisualizationContainer.Navigator.CopyToClipboardCommand, + null, + true, + this.Name)); + + contextMenu.Items.Add(copyToClipboardMenuItem); contextMenu.Items.Add(new Separator()); // Add show session info menu diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/ViewModels/StreamContainerTreeNode.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/ViewModels/StreamContainerTreeNode.cs deleted file mode 100644 index 89cadb05f..000000000 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/ViewModels/StreamContainerTreeNode.cs +++ /dev/null @@ -1,718 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. - -namespace Microsoft.Psi.Visualization.ViewModels -{ - using System; - using System.Collections.Generic; - using System.Collections.ObjectModel; - using System.ComponentModel; - using System.Diagnostics; - using System.Linq; - using System.Windows.Controls; - using System.Windows.Input; - using System.Windows.Media; - using GalaSoft.MvvmLight.CommandWpf; - using Microsoft.Psi.Diagnostics; - using Microsoft.Psi.PsiStudio.Common; - using Microsoft.Psi.Visualization; - using Microsoft.Psi.Visualization.Base; - using Microsoft.Psi.Visualization.Helpers; - - /// - /// Implements a node in the dataset tree that represents a stream container. - /// - /// - /// This class also acts as the base class for the hierarchy of stream tree nodes. - /// The class models a regular tree node corresponding - /// to a stream in the partition, whereas other classes specialize these further to - /// represent derived streams, such as - /// and . - /// - public class StreamContainerTreeNode : ObservableTreeNodeObject - { - private readonly ObservableCollection internalChildren; - private readonly ReadOnlyObservableCollection children; - - private string auxiliaryInfo = string.Empty; - - private RelayCommand mouseDoubleClickCommand; - private RelayCommand contextMenuOpeningCommand; - - /// - /// Initializes a new instance of the class. - /// - /// The partition for the container tree node. - /// The path to the container tree node. - /// The name of the container tree node. - public StreamContainerTreeNode(PartitionViewModel partitionViewModel, string path, string name) - { - this.PartitionViewModel = partitionViewModel; - this.PartitionViewModel.PropertyChanged += this.OnPartitionViewModelPropertyChanged; - this.DatasetViewModel.PropertyChanged += this.OnDatasetViewModelPropertyChanged; - - this.Path = path; - this.Name = name; - - this.internalChildren = new ObservableCollection(); - this.children = new ReadOnlyObservableCollection(this.internalChildren); - } - - /// - /// Finalizes an instance of the class. - /// - ~StreamContainerTreeNode() - { - this.PartitionViewModel.PropertyChanged -= this.OnPartitionViewModelPropertyChanged; - this.DatasetViewModel.PropertyChanged -= this.OnDatasetViewModelPropertyChanged; - } - - /// - /// Gets the dataset where this stream tree node can be found. - /// - [Browsable(false)] - public DatasetViewModel DatasetViewModel => this.PartitionViewModel.SessionViewModel.DatasetViewModel; - - /// - /// Gets the session where this stream tree node can be found. - /// - [Browsable(false)] - public SessionViewModel SessionViewModel => this.PartitionViewModel.SessionViewModel; - - /// - /// Gets the partition where this stream tree node can be found. - /// - [Browsable(false)] - public PartitionViewModel PartitionViewModel { get; private set; } - - /// - /// Gets the path of the stream tree node. - /// - [Browsable(false)] - public string Path { get; private set; } - - /// - /// Gets the name of the stream tree node. - /// - [Browsable(false)] - public string Name { get; private set; } - - /// - /// Gets the collection of children for this stream tree node. - /// - [Browsable(false)] - public ReadOnlyObservableCollection Children => this.children; - - /// - /// Gets the time interval of the stream(s) subsumed by this stream container tree node. - /// - [Browsable(false)] - public TimeInterval SubsumedTimeInterval => new (this.SubsumedOpenedTime, this.SubsumedClosedTime); - - /// - /// Gets the command that executes when opening the stream tree node context menu. - /// - [Browsable(false)] - public RelayCommand ContextMenuOpeningCommand => - this.contextMenuOpeningCommand ??= new RelayCommand( - grid => - { - var contextMenu = new ContextMenu(); - this.PopulateContextMenu(contextMenu); - grid.ContextMenu = contextMenu; - }); - - /// - /// Gets the command that executes when double clicking on the stream tree node. - /// - [Browsable(false)] - public RelayCommand MouseDoubleClickCommand => - this.mouseDoubleClickCommand ??= new RelayCommand(e => this.OnMouseDoubleClick(e)); - - /// - /// Gets the name to display in the stream tree. - /// - [Browsable(false)] - public virtual string DisplayName => this.Name; - - /// - /// Gets or sets the auxiliary info to display. - /// - [Browsable(false)] - public virtual string AuxiliaryInfo - { - get => this.auxiliaryInfo; - protected set => this.Set(nameof(this.AuxiliaryInfo), ref this.auxiliaryInfo, value); - } - - /// - /// Gets a value indicating whether this node is in the current session. - /// - [Browsable(false)] - public bool IsInCurrentSession => this.SessionViewModel.IsCurrentSession; - - /// - /// Gets the path to the stream's icon. - /// - [Browsable(false)] - public virtual string IconSource => this.PartitionViewModel.IsLivePartition ? IconSourcePath.GroupLive : IconSourcePath.Group; - - /// - /// Gets the opacity of the stream tree node. (Opacity is lowered for all nodes in sessions that are not the current session). - /// - [Browsable(false)] - public virtual double UiElementOpacity => this.SessionViewModel.UiElementOpacity; - - /// - /// Gets the color to use when rendering the tree node. - /// - [Browsable(false)] - public virtual Brush ForegroundBrush => new SolidColorBrush(Colors.White); - - /// - /// Gets a value indicating whether this container has any non-derived children. - /// - [Browsable(false)] - public bool HasNonDerivedChildren => this.children.Any(c => c is not DerivedStreamTreeNode); - - /// - /// Gets the first opened time for the stream(s) subsumed by the tree node. - /// - [Browsable(false)] - public virtual DateTime SubsumedOpenedTime - { - get - { - var available = this.children.Where(c => c is not DerivedStreamTreeNode && c.SubsumedOpenedTime != DateTime.MinValue); - return available.Any() ? available.Min(c => c.SubsumedOpenedTime) : DateTime.MinValue; - } - } - - /// - /// Gets the last closed time for the stream(s) subsumed by the tree node. - /// - [Browsable(false)] - public virtual DateTime SubsumedClosedTime - { - get - { - var available = this.children.Where(c => c is not DerivedStreamTreeNode && c.SubsumedClosedTime != DateTime.MaxValue); - return available.Any() ? available.Max(c => c.SubsumedClosedTime) : DateTime.MaxValue; - } - } - - /// - /// Gets the originating time interval for the stream(s) subsumed by the tree node. - /// - [Browsable(false)] - public virtual TimeInterval SubsumedOriginatingTimeInterval => - new (this.SubsumedFirstMessageOriginatingTime, this.SubsumedLastMessageOriginatingTime); - - /// - /// Gets the originating time of the first message in the stream(s) subsumed by the tree node. - /// - [Browsable(false)] - public virtual DateTime SubsumedFirstMessageOriginatingTime - { - get - { - var available = this.children.Where(c => c is not DerivedStreamTreeNode && c.SubsumedFirstMessageOriginatingTime != DateTime.MinValue); - return available.Any() ? available.Min(c => c.SubsumedFirstMessageOriginatingTime) : DateTime.MinValue; - } - } - - /// - /// Gets the creation time of the first message in the stream(s) subsumed by the tree node. - /// - [Browsable(false)] - public virtual DateTime SubsumedFirstMessageCreationTime - { - get - { - var available = this.children.Where(c => c is not DerivedStreamTreeNode && c.SubsumedFirstMessageCreationTime != DateTime.MinValue); - return available.Any() ? available.Min(c => c.SubsumedFirstMessageCreationTime) : DateTime.MinValue; - } - } - - /// - /// Gets the originating time of the last message in the stream(s) subsumed by the tree node. - /// - [Browsable(false)] - public virtual DateTime SubsumedLastMessageOriginatingTime - { - get - { - var available = this.children.Where(c => c is not DerivedStreamTreeNode && c.SubsumedLastMessageOriginatingTime != DateTime.MaxValue); - return available.Any() ? available.Max(c => c.SubsumedLastMessageOriginatingTime) : DateTime.MaxValue; - } - } - - /// - /// Gets the creation time of the last message in the stream(s) subsumed by the tree node. - /// - [Browsable(false)] - public virtual DateTime SubsumedLastMessageCreationTime - { - get - { - var available = this.children.Where(c => c is not DerivedStreamTreeNode && c.SubsumedLastMessageCreationTime != DateTime.MaxValue); - return available.Any() ? available.Max(c => c.SubsumedLastMessageCreationTime) : DateTime.MaxValue; - } - } - - /// - /// Gets the total number of messages in the stream(s) subsumed by the tree node. - /// - [DisplayName("Subsumed Message Count")] - [Description("The total number of messages in the stream(s) subsumed by the tree node.")] - public virtual long SubsumedMessageCount - => this.children.Where(c => c is not DerivedStreamTreeNode).Sum(c => c.SubsumedMessageCount); - - /// - /// Gets the average message latency for the stream(s) subsumed by the tree node. - /// - [DisplayName("Subsumed Avg. Message Latency (ms)")] - [Description("The average latency (in milliseconds) of messages in the stream(s) subsumed by the tree node.")] - public virtual double SubsumedAverageMessageLatencyMs => - this.SubsumedMessageCount > 0 ? - this.children.Where(c => c is not DerivedStreamTreeNode && c.SubsumedMessageCount > 0).Sum(c => c.SubsumedMessageCount * c.SubsumedAverageMessageLatencyMs) / this.SubsumedMessageCount : - double.NaN; - - /// - /// Gets the average message size for the stream(s) subsumed by the tree node. - /// - [DisplayName("Subsumed Avg. Message Size")] - [Description("The average size (in bytes) of messages in the stream(s) subsumed by the tree node.")] - public virtual double SubsumedAverageMessageSize => - this.SubsumedMessageCount > 0 ? - this.children.Where(c => c is not DerivedStreamTreeNode && c.SubsumedMessageCount > 0).Sum(c => c.SubsumedMessageCount * c.SubsumedAverageMessageSize) / this.SubsumedMessageCount : - double.NaN; - - /// - /// Gets the total data size in the stream(s) subsumed by the tree node. - /// - [DisplayName("Subsumed Size")] - [Description("The size (in bytes) of data in the stream(s) subsumed by the tree node.")] - public virtual long SubsumedSize - => this.children.Where(c => c is not DerivedStreamTreeNode).Sum(c => c.SubsumedSize); - - /// - /// Gets a string representation of the first opened time for the stream(s) subsumed by the tree node. - /// - [DisplayName("Subsumed OpenedTime")] - [Description("The first opened time for the stream(s) subsumed by the tree node.")] - public virtual string SubsumedOpenedTimeString - => DateTimeFormatHelper.FormatDateTime(this.SubsumedOpenedTime); - - /// - /// Gets a string representation of the last closed time for the stream(s) subsumed by the tree node. - /// - [DisplayName("Subsumed ClosedTime")] - [Description("The last closed time for the stream(s) subsumed by the tree node.")] - public virtual string SubsumedClosedTimeString - => DateTimeFormatHelper.FormatDateTime(this.SubsumedClosedTime); - - /// - /// Gets a string representation of the originating time of the first message in the stream(s) subsumed by the tree node. - /// - [DisplayName("Subsumed First Message OriginatingTime")] - [Description("The originating time of the first message in the stream(s) subsumed by the tree node.")] - public virtual string SubsumedFirstMessageOriginatingTimeString - => DateTimeFormatHelper.FormatDateTime(this.SubsumedFirstMessageOriginatingTime); - - /// - /// Gets a string representation of the time of the first message in the stream(s) subsumed by the tree node. - /// - [DisplayName("Subsumed First Message CreationTime")] - [Description("The creation time of the first message in the stream(s) subsumed by the tree node.")] - public virtual string SubsumedFirstMessageCreationTimeString - => DateTimeFormatHelper.FormatDateTime(this.SubsumedFirstMessageCreationTime); - - /// - /// Gets a string representation of the originating time of the last message in the stream(s) subsumed by the tree node. - /// - [DisplayName("Subsumed Last Message OriginatingTime")] - [Description("The originating time of the last message in the stream(s) subsumed by the tree node.")] - public virtual string SubsumedLastMessageOriginatingTimeString - => DateTimeFormatHelper.FormatDateTime(this.SubsumedLastMessageOriginatingTime); - - /// - /// Gets a string representation of the time of the last message in the stream(s) subsumed by the tree node. - /// - [DisplayName("Subsumed Last Message CreationTime")] - [Description("The creation time of the last message in the stream(s) subsumed by the tree node.")] - public virtual string SubsumedLastMessageCreationTimeString - => DateTimeFormatHelper.FormatDateTime(this.SubsumedLastMessageCreationTime); - - /// - /// Gets the internal collection of children for the this stream tree node. - /// - [Browsable(false)] - protected ObservableCollection InternalChildren => this.internalChildren; - - /// - /// Adds a new stream tree node based on the specified stream as child of this node. - /// - /// The stream to add to the tree. - /// A reference to the new stream tree node. - public StreamTreeNode AddStreamTreeNode(IStreamMetadata streamMetadata) => - this.AddStreamTreeNode(streamMetadata.Name.Split('.'), streamMetadata); - - /// - /// Adds a new container tree node to the set of children. - /// - /// The container tree node to add. - public void AddChildTreeNode(StreamContainerTreeNode treeNode) => - this.InternalChildren.Add(treeNode); - - /// - /// Selects a tree node and expands all nodes on the path to it. - /// - /// The path of the node to select. - /// True if the node was found, otherwise false. - public bool SelectTreeNode(string nodePath) => - this.SelectNode(nodePath.Split('.')); - - /// - /// Finds a stream tree node by stream name. - /// - /// The name of the stream to search for. - /// A stream tree node, or null if the stream was not found. - public StreamTreeNode FindStreamTreeNode(string streamName) => - this.FindStreamContainerTreeNode(streamName.Split('.')) as StreamTreeNode; - - /// - /// Finds a stream container tree node by full path. - /// - /// The path to the node. - /// A stream container tree node, or null if the node was not found. - public StreamContainerTreeNode FindStreamContainerTreeNode(string path) => - this.FindStreamContainerTreeNode(path.Split('.')); - - /// - /// Expands this node and all of its child nodes recursively. - /// - public void ExpandAll() - { - foreach (var child in this.children) - { - child.ExpandAll(); - } - - this.IsTreeNodeExpanded = true; - } - - /// - /// Collapses this node and all of its child nodes recursively. - /// - public void CollapseAll() - { - this.IsTreeNodeExpanded = false; - - foreach (var child in this.children) - { - child.CollapseAll(); - } - } - - /// - public override string ToString() => $"Node: {this.Name}"; - - /// - /// Event handler for partition property changed. - /// - /// The sender. - /// The event arguments. - protected virtual void OnPartitionViewModelPropertyChanged(object sender, PropertyChangedEventArgs e) - { - if (e.PropertyName == nameof(this.PartitionViewModel.IsLivePartition)) - { - this.RaisePropertyChanged(nameof(this.IconSource)); - } - } - - /// - /// Event handler for dataset view model property changed. - /// - /// The sender. - /// The event arguments. - protected virtual void OnDatasetViewModelPropertyChanged(object sender, PropertyChangedEventArgs e) - { - if (e.PropertyName == nameof(this.DatasetViewModel.CurrentSessionViewModel)) - { - this.RaisePropertyChanged(nameof(this.UiElementOpacity)); - } - else if (e.PropertyName == nameof(this.DatasetViewModel.ShowAuxiliaryStreamInfo)) - { - this.UpdateAuxiliaryInfo(); - } - } - - /// - /// Handler for a double-click event on the stream container tree node. - /// - /// The mouse button event arguments. - protected virtual void OnMouseDoubleClick(MouseButtonEventArgs e) - { - } - - /// - /// Updates the auxiliary info to be displayed. - /// - protected virtual void UpdateAuxiliaryInfo() - { - switch (this.DatasetViewModel.ShowAuxiliaryStreamInfo) - { - case AuxiliaryStreamInfo.None: - this.AuxiliaryInfo = string.Empty; - break; - case AuxiliaryStreamInfo.Size: - this.AuxiliaryInfo = $"[{SizeFormatHelper.FormatSize(this.SubsumedSize)}]"; - break; - case AuxiliaryStreamInfo.DataThroughputPerHour: - this.AuxiliaryInfo = $"[{SizeFormatHelper.FormatThroughput(this.SubsumedSize / (this.SubsumedClosedTime - this.SubsumedOpenedTime).TotalHours, "hour")}]"; - break; - case AuxiliaryStreamInfo.DataThroughputPerMinute: - this.AuxiliaryInfo = $"[{SizeFormatHelper.FormatThroughput(this.SubsumedSize / (this.SubsumedClosedTime - this.SubsumedOpenedTime).TotalMinutes, "min")}]"; - break; - case AuxiliaryStreamInfo.DataThroughputPerSecond: - this.AuxiliaryInfo = $"[{SizeFormatHelper.FormatThroughput(this.SubsumedSize / (this.SubsumedClosedTime - this.SubsumedOpenedTime).TotalSeconds, "sec")}]"; - break; - case AuxiliaryStreamInfo.MessageCountThroughputPerHour: - this.AuxiliaryInfo = $"[{this.SubsumedMessageCount / (this.SubsumedClosedTime - this.SubsumedOpenedTime).TotalHours:0.01}]"; - break; - case AuxiliaryStreamInfo.MessageCountThroughputPerMinute: - this.AuxiliaryInfo = $"[{this.SubsumedMessageCount / (this.SubsumedClosedTime - this.SubsumedOpenedTime).TotalMinutes:0.01}]"; - break; - case AuxiliaryStreamInfo.MessageCountThroughputPerSecond: - this.AuxiliaryInfo = $"[{this.SubsumedMessageCount / (this.SubsumedClosedTime - this.SubsumedOpenedTime).TotalSeconds:0.01}]"; - break; - case AuxiliaryStreamInfo.MessageCount: - this.AuxiliaryInfo = this.SubsumedMessageCount == 0 ? "[0]" : $"[{this.SubsumedMessageCount:0,0}]"; - break; - case AuxiliaryStreamInfo.AverageMessageLatencyMs: - this.AuxiliaryInfo = $"[{SizeFormatHelper.FormatLatencyMs(this.SubsumedAverageMessageLatencyMs)}]"; - break; - case AuxiliaryStreamInfo.AverageMessageSize: - this.AuxiliaryInfo = $"[{SizeFormatHelper.FormatSize(this.SubsumedAverageMessageSize)}]"; - break; - default: - break; - } - } - - /// - /// Populates a context menu with items for this tree node. - /// - /// The context menu to populate. - protected virtual void PopulateContextMenu(ContextMenu contextMenu) - { - // Add the visualize session context menu if the stream is not in the currently visualized session - if (!this.SessionViewModel.IsCurrentSession) - { - if (contextMenu.Items.Count > 0) - { - contextMenu.Items.Add(new Separator()); - } - - contextMenu.Items.Add(MenuItemHelper.CreateMenuItem(string.Empty, ContextMenuName.VisualizeSession, this.SessionViewModel.VisualizeSessionCommand)); - } - - this.PopulateContextMenuWithExpandAndCollapseAll(contextMenu); - this.PopulateContextMenuWithShowStreamInfo(contextMenu); - } - - /// - /// Populates a context menu with the commands for showing stream info. - /// - /// The context menu to populate. - protected void PopulateContextMenuWithShowStreamInfo(ContextMenu contextMenu) - { - if (contextMenu.Items.Count > 0) - { - contextMenu.Items.Add(new Separator()); - } - - // Add run batch processing task menu - var showStreamInfoMenuItem = MenuItemHelper.CreateMenuItem(string.Empty, "Show Streams Info", null); - foreach (var auxiliaryStreamInfo in Enum.GetValues(typeof(AuxiliaryStreamInfo))) - { - var auxiliaryStreamInfoValue = (AuxiliaryStreamInfo)auxiliaryStreamInfo; - var auxiliaryStreamInfoName = auxiliaryStreamInfoValue switch - { - AuxiliaryStreamInfo.None => "None", - AuxiliaryStreamInfo.MessageCount => "Message Count", - AuxiliaryStreamInfo.AverageMessageSize => "Average Message Size", - AuxiliaryStreamInfo.AverageMessageLatencyMs => "Average Message Latency", - AuxiliaryStreamInfo.Size => "Size", - AuxiliaryStreamInfo.DataThroughputPerHour => "Throughput (bytes per hour)", - AuxiliaryStreamInfo.DataThroughputPerMinute => "Throughput (bytes per minute)", - AuxiliaryStreamInfo.DataThroughputPerSecond => "Throughput (bytes per second)", - AuxiliaryStreamInfo.MessageCountThroughputPerHour => "Throughput (messages per hour)", - AuxiliaryStreamInfo.MessageCountThroughputPerMinute => "Throughput (messages per minute)", - AuxiliaryStreamInfo.MessageCountThroughputPerSecond => "Throughput (messages per second)", - _ => throw new NotImplementedException(), - }; - - showStreamInfoMenuItem.Items.Add( - MenuItemHelper.CreateMenuItem( - this.DatasetViewModel.ShowAuxiliaryStreamInfo == auxiliaryStreamInfoValue ? IconSourcePath.Checkmark : null, - auxiliaryStreamInfoName, - new VisualizationCommand(s => this.DatasetViewModel.ShowAuxiliaryStreamInfo = s), - commandParameter: auxiliaryStreamInfoValue)); - } - - contextMenu.Items.Add(showStreamInfoMenuItem); - } - - /// - /// Populates a context menu with the expand and collapse all items. - /// - /// The context menu to populate. - protected void PopulateContextMenuWithExpandAndCollapseAll(ContextMenu contextMenu) - { - if (!this.InternalChildren.Any()) - { - return; - } - - if (contextMenu.Items.Count > 0) - { - contextMenu.Items.Add(new Separator()); - } - - contextMenu.Items.Add( - MenuItemHelper.CreateMenuItem( - IconSourcePath.ExpandAllNodes, - ContextMenuName.ExpandAllNodes, - new VisualizationCommand(() => this.ExpandAll()), - isEnabled: this.InternalChildren.Any())); - - contextMenu.Items.Add( - MenuItemHelper.CreateMenuItem( - IconSourcePath.CollapseAllNodes, - ContextMenuName.CollapseAllNodes, - new VisualizationCommand(() => this.CollapseAll()), - isEnabled: this.InternalChildren.Any())); - } - - /// - /// Creates a stream tree node. - /// - /// The partition for the stream tree node. - /// The path to the stream tree node. - /// The name of the stream tree node. - /// The stream metadata. - /// The stream tree node. - private StreamTreeNode CreateStreamTreeNode(PartitionViewModel partitionViewModel, string path, string name, IStreamMetadata streamMetadata) - { - var dataType = VisualizationContext.Instance.GetDataType(streamMetadata.TypeName); - if (dataType == typeof(PipelineDiagnostics)) - { - return new PipelineDiagnosticsStreamTreeNode(partitionViewModel, path, name, streamMetadata); - } - else - { - return new StreamTreeNode(partitionViewModel, path, name, streamMetadata); - } - } - - /// - /// Helper method that adds a stream tree node to the container tree node, recursively. - /// - /// The path to the stream tree node. - /// The stream metadata. - /// The resulting stream tree node. - private StreamTreeNode AddStreamTreeNode(IEnumerable path, IStreamMetadata streamMetadata) - { - var firstPathElement = path.First(); - - if (this.InternalChildren.FirstOrDefault(p => p.Name == firstPathElement) is not StreamContainerTreeNode streamContainerTreeNode) - { - streamContainerTreeNode = new StreamContainerTreeNode(this.PartitionViewModel, this.Path == null ? firstPathElement : $"{this.Path}.{firstPathElement}", firstPathElement); - this.InternalChildren.Add(streamContainerTreeNode); - } - - // if we are at the last segment of the path name then we are at the leaf node - if (path.Count() == 1) - { - Debug.Assert(streamContainerTreeNode is not StreamTreeNode, "There should never be two leaf nodes"); - - // find the index of the streamContainer, remove it, and add a stream tree node at that index - var index = this.InternalChildren.IndexOf(streamContainerTreeNode); - this.InternalChildren.Remove(streamContainerTreeNode); - - var streamTreeNode = this.CreateStreamTreeNode(this.PartitionViewModel, streamContainerTreeNode.Path, streamContainerTreeNode.Name, streamMetadata); - - // add any children the previous container might have had to the stream tree node - foreach (var child in streamContainerTreeNode.Children) - { - streamTreeNode.AddChildTreeNode(child); - } - - this.InternalChildren.Insert(index, streamTreeNode); - - return streamTreeNode; - } - - // we are not at the last segment so recurse in - return streamContainerTreeNode.AddStreamTreeNode(path.Skip(1), streamMetadata); - } - - /// - /// Recursively selects a node specified by a path. - /// - /// The path. - /// True if the node was found. - private bool SelectNode(IEnumerable path) - { - var firstPathElement = path.First(); - var child = this.InternalChildren.FirstOrDefault(p => p.Name == firstPathElement); - if (child == default) - { - return false; - } - - if (path.Count() == 1) - { - child.IsTreeNodeSelected = true; - this.IsTreeNodeExpanded = true; - return true; - } - else - { - bool result = child.SelectNode(path.Skip(1)); - if (result) - { - this.IsTreeNodeExpanded = true; - } - - return result; - } - } - - /// - /// Finds a stream container tree node specified by a path. - /// - /// The path. - /// The corresponding stream container tree node. - private StreamContainerTreeNode FindStreamContainerTreeNode(IEnumerable path) - { - var firstPathElement = path.First(); - var child = this.InternalChildren.FirstOrDefault(p => p.Name == firstPathElement); - if (child == default) - { - return null; - } - - if (path.Count() == 1) - { - return child as StreamContainerTreeNode; - } - else - { - return child.FindStreamContainerTreeNode(path.Skip(1)); - } - } - } -} diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/ViewModels/StreamTreeNode.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/ViewModels/StreamTreeNode.cs index ba3c44612..9879255ed 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/ViewModels/StreamTreeNode.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/ViewModels/StreamTreeNode.cs @@ -5,27 +5,47 @@ namespace Microsoft.Psi.Visualization.ViewModels { using System; using System.Collections.Generic; + using System.Collections.ObjectModel; using System.ComponentModel; using System.Linq; using System.Reflection; + using System.Windows; using System.Windows.Controls; using System.Windows.Input; + using System.Windows.Media; + using GalaSoft.MvvmLight.CommandWpf; using Microsoft.Psi.Audio; + using Microsoft.Psi.Data; using Microsoft.Psi.Data.Annotations; + using Microsoft.Psi.Diagnostics; using Microsoft.Psi.PsiStudio.Common; using Microsoft.Psi.PsiStudio.TypeSpec; using Microsoft.Psi.Visualization; using Microsoft.Psi.Visualization.Adapters; + using Microsoft.Psi.Visualization.Base; using Microsoft.Psi.Visualization.Data; using Microsoft.Psi.Visualization.Helpers; using Microsoft.Psi.Visualization.VisualizationObjects; using Microsoft.Psi.Visualization.VisualizationPanels; + using Microsoft.Psi.Visualization.Windows; /// /// Implements a node in the dataset tree that represents a stream. /// - public class StreamTreeNode : StreamContainerTreeNode + /// + /// Stream tree nodes can represent (1) source streams that are present in a store, + /// (2) derived streams, i.e., streams computed from a source stream based on a + /// derived stream adapter, or (3) stream containers (that subsume other streams). + /// + public class StreamTreeNode : ObservableTreeNodeObject { + private readonly ObservableCollection internalChildren; + + private string auxiliaryInfo = string.Empty; + + private RelayCommand mouseDoubleClickCommand; + private RelayCommand contextMenuOpeningCommand; + private bool isDirty = false; private bool? supplementalMetadataTypeIsKnown = null; @@ -33,389 +53,798 @@ public class StreamTreeNode : StreamContainerTreeNode /// Initializes a new instance of the class. /// /// The partition for the stream tree node. - /// The path to the stream tree node. - /// The name of the stream tree node. - /// The stream metadata. - public StreamTreeNode(PartitionViewModel partitionViewModel, string path, string name, IStreamMetadata streamMetadata) - : base(partitionViewModel, path, name) + /// The full, hierarchical name of the stream tree node. + /// The source stream metadata. + /// The adapter type for constructing a derived stream tree node. + /// The adapter arguments for constructing a derived stream tree node. + private StreamTreeNode( + PartitionViewModel partitionViewModel, + string fullName, + IStreamMetadata sourceStreamMetadata, + Type derivedStreamAdapterType, + object[] derivedStreamAdapterArguments) { - this.SourceStreamMetadata = streamMetadata; - this.DataTypeName = this.SourceStreamMetadata.TypeName; + // Sanity check that if we have a derived stream, we have an adapter + if (sourceStreamMetadata != null && ((sourceStreamMetadata.Name != fullName) != (derivedStreamAdapterType != null))) + { + throw new ArgumentException("Attempting to construct a derived stream tree node without an adapter or a regular stream tree node with an adapter."); + } + + this.PartitionViewModel = partitionViewModel; + this.PartitionViewModel.PropertyChanged += this.OnPartitionViewModelPropertyChanged; + this.DatasetViewModel.PropertyChanged += this.OnDatasetViewModelPropertyChanged; + + this.FullName = fullName; + this.Name = this.FullName?.Split('.').Last(); + + this.internalChildren = new (); + this.Children = new ReadOnlyObservableCollection(this.internalChildren); + + this.SourceStreamMetadata = sourceStreamMetadata; + this.DerivedStreamAdapterType = derivedStreamAdapterType; + this.DerivedStreamAdapterArguments = derivedStreamAdapterArguments; + + // Compute the data type for the node, based on whether or not we have a derived stream adapter + if (this.DerivedStreamAdapterType != null) + { + var derivedStreamAdapter = (IStreamAdapter)Activator.CreateInstance(this.DerivedStreamAdapterType, this.DerivedStreamAdapterArguments); + this.DataType = derivedStreamAdapter.DestinationType; + } + else if (sourceStreamMetadata != null) + { + this.DataType = VisualizationContext.Instance.GetDataType(this.SourceStreamMetadata.TypeName); + } + + this.HasSourceStreamDescendants = false; } /// - /// Gets or sets the metadata of the source data stream. + /// Finalizes an instance of the class. /// - [Browsable(false)] - public IStreamMetadata SourceStreamMetadata { get; protected set; } + ~StreamTreeNode() + { + this.PartitionViewModel.PropertyChanged -= this.OnPartitionViewModelPropertyChanged; + this.DatasetViewModel.PropertyChanged -= this.OnDatasetViewModelPropertyChanged; + } /// - /// Gets or sets the type of data represented by this stream tree node. + /// Gets the dataset for the stream tree node. /// [Browsable(false)] - public string DataTypeName { get; protected set; } + public DatasetViewModel DatasetViewModel => this.PartitionViewModel.SessionViewModel.DatasetViewModel; /// - /// Gets or sets a value indicating whether the stream has unsaved changes. + /// Gets the session for the stream tree node. /// [Browsable(false)] - public bool IsDirty - { - get => this.isDirty; - set - { - this.RaisePropertyChanging(nameof(this.DisplayName)); - this.isDirty = value; - this.RaisePropertyChanged(nameof(this.DisplayName)); - } - } + public SessionViewModel SessionViewModel => this.PartitionViewModel.SessionViewModel; /// - /// Gets a value indicating whether the stream's supplemental metadata (if any) is known, readable type. + /// Gets the partition for the stream tree node. /// [Browsable(false)] - public bool SupplementalMetadataTypeIsKnown - { - get - { - // If there is no supplemental metadata for the stream, then everything is fine. - if (string.IsNullOrWhiteSpace(this.SourceStreamMetadata.SupplementalMetadataTypeName)) - { - return true; - } - - // If we've not tried to read the supplemental metadata yet, do so now. - if (!this.supplementalMetadataTypeIsKnown.HasValue) - { - try - { - // Attempt to read the supplemental metadata for the stream. - MethodInfo methodInfo = DataManager.Instance.GetType().GetMethod( - nameof(DataManager.GetSupplementalMetadataByName), - new Type[] { typeof(string), typeof(string), typeof(Type), typeof(string) }); - - MethodInfo genericMethodInfo = methodInfo.MakeGenericMethod(TypeResolutionHelper.GetVerifiedType(this.SourceStreamMetadata.SupplementalMetadataTypeName)); - - var parameters = new object[] - { - this.PartitionViewModel.StoreName, - this.PartitionViewModel.StorePath, - TypeResolutionHelper.GetVerifiedType(this.PartitionViewModel.Partition.StreamReaderTypeName), - this.SourceStreamName, - }; - genericMethodInfo.Invoke(DataManager.Instance, parameters); - - // Success - this.supplementalMetadataTypeIsKnown = true; - } - catch (Exception) - { - this.supplementalMetadataTypeIsKnown = false; - } - } - - return this.supplementalMetadataTypeIsKnown.Value; - } - } + public PartitionViewModel PartitionViewModel { get; } /// - /// Gets a value indicating whether the node represends a stream backed by a \psi store. + /// Gets the full, hierarchical name of the stream tree node. /// [Browsable(false)] - public bool IsPsiStream => this.SourceStreamMetadata is PsiStreamMetadata; + public string FullName { get; } /// - /// Gets a value indicating whether the node represends a indexed stream backed by a \psi store. + /// Gets the name of the stream tree node. /// [Browsable(false)] - public bool IsIndexedPsiStream => this.SourceStreamMetadata is PsiStreamMetadata psiStreamMetadata && psiStreamMetadata.IsIndexed; + public string Name { get; } /// - /// Gets the originating time interval (earliest to latest) of the messages under this stream. + /// Gets the name to display in the stream tree. /// [Browsable(false)] - public TimeInterval OriginatingTimeInterval => new (this.SourceStreamFirstMessageOriginatingTime, this.SourceStreamLastMessageOriginatingTime); + public string DisplayString => this.IsDirty ? $"{this.Name}*" : this.Name; /// - /// Gets the id of the data stream. + /// Gets the metadata of the source data stream. /// - [DisplayName("Source Stream Id")] - [Description("The id of the source stream.")] - public int SourceStreamId => this.SourceStreamMetadata.Id; + [Browsable(false)] + public IStreamMetadata SourceStreamMetadata { get; } /// - /// Gets the stream name of this stream tree node. + /// Gets the type of data represented by this stream tree node. /// - [DisplayName("Source Stream Name")] - [Description("The name of the source stream.")] - public string SourceStreamName => this.SourceStreamMetadata.Name; + [Browsable(false)] + public Type DataType { get; } /// - /// Gets the type of messages in this stream tree node. + /// Gets a value indicating whether type of data represented by this stream tree node is nullable. /// - [DisplayName("Source Stream Type")] - [Description("The type of messages in the source stream.")] - public string SourceStreamTypeNameSimplified => TypeSpec.Simplify(this.SourceStreamMetadata.TypeName); + [Browsable(false)] + public bool IsNullableDataType => this.DataType != null && Nullable.GetUnderlyingType(this.DataType) != null; /// - /// Gets the type of data represented by this stream tree node. + /// Gets a string representation of the type of data represented by this stream tree node. /// [DisplayName("Data Type")] - [Description("The type of data contained by this node.")] - public string DataTypeNameSimplified => TypeSpec.Simplify(this.DataTypeName); - - /// - public override double SubsumedAverageMessageSize => - this.HasNonDerivedChildren ? - ((base.SubsumedMessageCount == 0 ? 0 : (base.SubsumedAverageMessageSize * base.SubsumedMessageCount)) + - (this.SourceStreamMessageCount == 0 ? 0 : (this.SourceStreamAverageMessageSize * this.SourceStreamMessageCount))) / - (base.SubsumedMessageCount + this.SourceStreamMessageCount) : - (this.SourceStreamMessageCount > 0 ? this.SourceStreamAverageMessageSize : double.NaN); + [Description("The type of data for this stream.")] + public string DataTypeDisplayString => this.DataType != null ? TypeSpec.Simplify(this.DataType.AssemblyQualifiedName) : "N/A"; /// - /// Gets the average message size in the stream. + /// Gets the adapter type for constructing the derived stream. /// - [DisplayName("Source Stream Avg. Message Size")] - [Description("The average size (in bytes) of messages in the source stream.")] - public virtual double SourceStreamAverageMessageSize => this.SourceStreamMessageCount > 0 ? this.SourceStreamMetadata.AverageMessageSize : double.NaN; - - /// - public override double SubsumedAverageMessageLatencyMs => - this.HasNonDerivedChildren ? - ((base.SubsumedMessageCount == 0 ? 0 : (base.SubsumedAverageMessageLatencyMs * base.SubsumedMessageCount)) + - (this.SourceStreamMessageCount == 0 ? 0 : (this.SourceStreamAverageMessageLatencyMs * this.SourceStreamMessageCount))) / - (base.SubsumedMessageCount + this.SourceStreamMessageCount) : - (this.SourceStreamMessageCount > 0 ? this.SourceStreamAverageMessageLatencyMs : double.NaN); + [Browsable(false)] + public Type DerivedStreamAdapterType { get; } /// - /// Gets the average latency of messages in the streams(s) subsumed by the tree node. + /// Gets a string representation of the adapter type for constructing the derived stream. /// - [DisplayName("Source Stream Avg. Message Latency (ms)")] - [Description("The average latency (in milliseconds) of messages in the source stream.")] - public virtual double SourceStreamAverageMessageLatencyMs => this.SourceStreamMessageCount > 0 ? this.SourceStreamMetadata.AverageMessageLatencyMs : double.NaN; - - /// - public override DateTime SubsumedOpenedTime => - this.HasNonDerivedChildren ? - new DateTime(Math.Min(base.SubsumedOpenedTime.Ticks, this.SourceStreamOpenedTime.Ticks)) : - this.SourceStreamOpenedTime; + [DisplayName("Derived Stream Adapter Type")] + [Description("The stream adapter used to compute the derived stream.")] + public string DerivedStreamAdapterTypeDisplayString => this.DerivedStreamAdapterType != null ? TypeSpec.Simplify(this.DerivedStreamAdapterType.AssemblyQualifiedName) : "N/A"; /// - /// Gets the time at which the stream was opened. + /// Gets the adapter parameters for constructing the derived stream. /// [Browsable(false)] - public virtual DateTime SourceStreamOpenedTime => this.SourceStreamMetadata.OpenedTime; - - /// - public override DateTime SubsumedClosedTime => - this.HasNonDerivedChildren ? - new DateTime(Math.Max(base.SubsumedClosedTime.Ticks, this.SourceStreamClosedTime.Ticks)) : - this.SourceStreamClosedTime; + public object[] DerivedStreamAdapterArguments { get; } /// - /// Gets the time at which the stream was closed. + /// Gets the collection of children for this stream tree node. /// [Browsable(false)] - public virtual DateTime SourceStreamClosedTime => this.SourceStreamMetadata.ClosedTime; - - /// - public override DateTime SubsumedFirstMessageOriginatingTime => - this.HasNonDerivedChildren ? - new DateTime(Math.Min(base.SubsumedFirstMessageOriginatingTime.Ticks, this.SourceStreamFirstMessageOriginatingTime.Ticks)) : - this.SourceStreamFirstMessageOriginatingTime; + public ReadOnlyObservableCollection Children { get; } /// - /// Gets the originating time of the first message in the stream. + /// Gets a value indicating whether this node corresponds to a stream. /// [Browsable(false)] - public virtual DateTime SourceStreamFirstMessageOriginatingTime => this.SourceStreamMetadata.FirstMessageOriginatingTime; - - /// - public override DateTime SubsumedFirstMessageCreationTime => - this.HasNonDerivedChildren ? - new DateTime(Math.Min(base.SubsumedFirstMessageCreationTime.Ticks, this.SourceStreamFirstMessageCreationTime.Ticks)) : - this.SourceStreamFirstMessageCreationTime; + public bool IsStream => this.SourceStreamMetadata != null; /// - /// Gets the creation time of the first message in the stream. + /// Gets a value indicating whether this node corresponds to a stream container. /// [Browsable(false)] - public virtual DateTime SourceStreamFirstMessageCreationTime => this.SourceStreamMetadata.FirstMessageCreationTime; - - /// - public override DateTime SubsumedLastMessageOriginatingTime => - this.HasNonDerivedChildren ? - new DateTime(Math.Max(base.SubsumedLastMessageOriginatingTime.Ticks, this.SourceStreamLastMessageOriginatingTime.Ticks)) : - this.SourceStreamLastMessageOriginatingTime; + public bool IsContainer => this.SourceStreamMetadata == null; /// - /// Gets the originating time of the last message in the stream. + /// Gets a value indicating whether this node corresponds to a stream backed by a \psi store. /// [Browsable(false)] - public virtual DateTime SourceStreamLastMessageOriginatingTime => this.SourceStreamMetadata.LastMessageOriginatingTime; + public bool IsPsiStream => + this.IsStream && + this.SourceStreamMetadata is PsiStreamMetadata; - /// - public override DateTime SubsumedLastMessageCreationTime => - this.HasNonDerivedChildren ? - new DateTime(Math.Max(base.SubsumedLastMessageCreationTime.Ticks, this.SourceStreamLastMessageCreationTime.Ticks)) : - this.SourceStreamLastMessageCreationTime; + /// + /// Gets a value indicating whether this node corresponds to an indexed stream backed by a \psi store. + /// + [Browsable(false)] + public bool IsIndexedPsiStream => + this.IsStream && + this.SourceStreamMetadata is PsiStreamMetadata psiStreamMetadata && + psiStreamMetadata.IsIndexed; /// - /// Gets the creation time of the last message in the stream. + /// Gets a value indicating whether this node corresponds to a source stream. /// [Browsable(false)] - public virtual DateTime SourceStreamLastMessageCreationTime => this.SourceStreamMetadata.LastMessageCreationTime; + public bool IsSourceStream => this.IsStream && this.DerivedStreamAdapterType == null; - /// - public override long SubsumedMessageCount => - this.HasNonDerivedChildren ? - base.SubsumedMessageCount + this.SourceStreamMessageCount : - this.SourceStreamMessageCount; + /// + /// Gets a value indicating whether this node corresponds to a derived stream. + /// + [DisplayName("Is Derived Stream")] + [Description("Indicates whether this is a derived stream.")] + public bool IsDerivedStream => this.IsStream && this.DerivedStreamAdapterType != null; /// - /// Gets the number of messages in the stream. + /// Gets a value indicating whether this node corresponds to a script derived stream. /// - [DisplayName("Source Stream Message Count")] - [Description("The total number of messages in the stream.")] - public virtual long SourceStreamMessageCount => this.SourceStreamMetadata.MessageCount; + [DisplayName("Is Script Derived Steam")] + [Description("Indicates whether this is a script derived stream.")] + public bool IsScriptDerivedStream + { + get + { + var lastAdapterType = this.DerivedStreamAdapterType; - /// - public override long SubsumedSize => - this.HasNonDerivedChildren ? - base.SubsumedSize + this.SourceStreamSize : - this.SourceStreamSize; + while (lastAdapterType != null && lastAdapterType.IsGenericType) + { + if (lastAdapterType.GetGenericTypeDefinition() == typeof(ScriptAdapter<,>)) + { + return true; + } + else if (lastAdapterType.GetGenericTypeDefinition() == typeof(ChainedStreamAdapter<,,,,>)) + { + // continue searching from the second chained adapter + lastAdapterType = lastAdapterType.GetGenericArguments()[4]; + } + else + { + lastAdapterType = null; + } + } + + return false; + } + } /// - /// Gets the total size of the messages in the stream. + /// Gets a value indicating whether this node has source stream descendants. /// - [DisplayName("Source Stream Size")] - [Description("The size (in bytes) of data in the stream.")] - public virtual long SourceStreamSize + [Browsable(false)] + public bool HasSourceStreamDescendants { get; private set; } + + /// + /// Gets the path to the icon for the node. + /// + [Browsable(false)] + public string IconSource { get { - if (this.SourceStreamMetadata is PsiStreamMetadata psiStreamMetadata) + if (this.SourceStreamMetadata == null) { - return psiStreamMetadata.MessageSizeCumulativeSum; + return this.PartitionViewModel.IsLivePartition ? IconSourcePath.GroupLive : IconSourcePath.Group; + } + else if (this.IsDerivedStream) + { + return this.PartitionViewModel.IsLivePartition ? IconSourcePath.DerivedStreamLive : IconSourcePath.DerivedStream; + } + else if (this.DataType == typeof(AudioBuffer)) + { + // Audio stream + return this.PartitionViewModel.IsLivePartition ? IconSourcePath.StreamAudioMutedLive : IconSourcePath.StreamAudioMuted; + } + else if (this.DataType == typeof(PipelineDiagnostics)) + { + return this.PartitionViewModel.IsLivePartition ? IconSourcePath.DiagnosticsLive : IconSourcePath.Diagnostics; + } + else if (this.DataType == typeof(TimeIntervalAnnotation)) + { + // Annotation + return IconSourcePath.Annotation; + } + else if (this.internalChildren.Any()) + { + // Group node that's also a stream + return this.PartitionViewModel.IsLivePartition ? IconSourcePath.StreamGroupLive : IconSourcePath.StreamGroup; } else { - return (long)(this.SourceStreamMetadata.AverageMessageSize * this.SourceStreamMetadata.MessageCount); + // Stream + return this.PartitionViewModel.IsLivePartition ? IconSourcePath.StreamLive : IconSourcePath.Stream; } } } + /// + /// Gets the brush to use when rendering the node. + /// + [Browsable(false)] + public Brush ForegroundBrush => ((this.IsStream && !this.IsDerivedStream) || this.HasSourceStreamDescendants) ? + new SolidColorBrush(Colors.White) : new SolidColorBrush(Colors.LightGray); + + /// + /// Gets the opacity to use when rendering the node. + /// + /// + /// Opacity is lowered for all nodes in sessions that are not the current session. + /// + [Browsable(false)] + public double UiElementOpacity => this.SessionViewModel.UiElementOpacity; + + /// + /// Gets the auxiliary info to display. + /// + [Browsable(false)] + public string AuxiliaryInfo + { + get => this.auxiliaryInfo; + private set => this.Set(nameof(this.AuxiliaryInfo), ref this.auxiliaryInfo, value); + } + + /// + /// Gets a value indicating whether this node is in the current session. + /// + [Browsable(false)] + public bool IsInCurrentSession => this.SessionViewModel.IsCurrentSession; + + /// + /// Gets the id of the source stream. + /// + [DisplayName("Source Stream Id")] + [Description("The id of the source stream.")] + public string SourceStreamIdString => this.SourceStreamMetadata != null ? this.SourceStreamMetadata.Id.ToString() : "N/A"; + + /// + /// Gets the name of the source stream. + /// + [DisplayName("Source Stream Name")] + [Description("The name of the source stream.")] + public string SourceStreamName => this.SourceStreamMetadata != null ? this.SourceStreamMetadata.Name : "N/A"; + + /// + /// Gets the type of messages in the source stream. + /// + [DisplayName("Source Stream Type")] + [Description("The type of messages in the source stream.")] + public string SourceStreamTypeDisplayString => this.SourceStreamMetadata != null ? TypeSpec.Simplify(this.SourceStreamMetadata.TypeName) : "N/A"; + + /// + /// Gets the type of messages in the source stream. + /// + [Browsable(false)] + public string SourceStreamTypeFullNameDisplayString => this.SourceStreamMetadata != null ? this.SourceStreamMetadata.TypeName : "N/A"; + + /// + /// Gets the source stream opened time. + /// + [Browsable(false)] + public DateTime? SourceStreamOpenedTime => this.SourceStreamMetadata?.OpenedTime; + /// /// Gets a string representation of the source stream opened time. /// [DisplayName("Source Stream OpenedTime")] [Description("The opened time for the source stream.")] - public virtual string SourceStreamOpenedTimeString - => DateTimeFormatHelper.FormatDateTime(this.SourceStreamOpenedTime); + public string SourceStreamOpenedTimeDisplayString + => this.SourceStreamOpenedTime != null ? DateTimeHelper.FormatDateTime(this.SourceStreamOpenedTime) : "N/A"; + + /// + /// Gets the source stream closed time. + /// + [Browsable(false)] + public DateTime? SourceStreamClosedTime => this.SourceStreamMetadata?.ClosedTime; /// /// Gets a string representation of the source stream closed time. /// [DisplayName("Source Stream ClosedTime")] [Description("The closed time for the source stream.")] - public virtual string SourceStreamClosedTimeString - => DateTimeFormatHelper.FormatDateTime(this.SourceStreamClosedTime); + public string SourceStreamClosedTimeDisplayString + => this.SourceStreamClosedTime != null ? DateTimeHelper.FormatDateTime(this.SourceStreamClosedTime) : "N/A"; + + /// + /// Gets the originating time of the first message in the source stream. + /// + [Browsable(false)] + public DateTime? SourceStreamFirstMessageOriginatingTime + => this.SourceStreamMetadata != null && this.SourceStreamMetadata.FirstMessageOriginatingTime != DateTime.MinValue ? + this.SourceStreamMetadata.FirstMessageOriginatingTime : null; /// - /// Gets a string representation of the originating time of the first message in the stream. + /// Gets a string representation of the originating time of the first message in the source stream. /// [DisplayName("Source Stream First Message OriginatingTime")] [Description("The originating time of the first message in the stream.")] - public virtual string SourceStreamFirstMessageOriginatingTimeString - => DateTimeFormatHelper.FormatDateTime(this.SourceStreamFirstMessageOriginatingTime); + public string SourceStreamFirstMessageOriginatingTimeDisplayString + => this.SourceStreamFirstMessageOriginatingTime != null ? DateTimeHelper.FormatDateTime(this.SourceStreamFirstMessageOriginatingTime) : "N/A"; + + /// + /// Gets the creation time of the first message in the source stream. + /// + [Browsable(false)] + public DateTime? SourceStreamFirstMessageCreationTime + => this.SourceStreamMetadata != null && this.SourceStreamMetadata.FirstMessageCreationTime != DateTime.MinValue ? + this.SourceStreamMetadata.FirstMessageCreationTime : null; /// - /// Gets a string representation of the time of the first message in the stream. + /// Gets a string representation of the time of the first message in the source stream. /// [DisplayName("Source Stream First Message CreationTime")] [Description("The creation time of the first message in the stream.")] - public virtual string SourceStreamFirstMessageCreationTimeString - => DateTimeFormatHelper.FormatDateTime(this.SourceStreamFirstMessageCreationTime); + public string SourceStreamFirstMessageCreationTimeDisplayString + => this.SourceStreamFirstMessageCreationTime != null ? DateTimeHelper.FormatDateTime(this.SourceStreamFirstMessageCreationTime) : "N/A"; + + /// + /// Gets the originating time of the last message in the source stream. + /// + [Browsable(false)] + public DateTime? SourceStreamLastMessageOriginatingTime + => this.SourceStreamMetadata != null && this.SourceStreamMetadata.LastMessageOriginatingTime != DateTime.MaxValue ? + this.SourceStreamMetadata.LastMessageOriginatingTime : null; /// - /// Gets a string representation of the originating time of the last message in the stream. + /// Gets a string representation of the originating time of the last message in the source stream. /// [DisplayName("Source Stream Last Message OriginatingTime")] [Description("The originating time of the last message in the stream.")] - public virtual string SourceStreamLastMessageOriginatingTimeString - => DateTimeFormatHelper.FormatDateTime(this.SourceStreamLastMessageOriginatingTime); + public string SourceStreamLastMessageOriginatingTimeDisplayString + => this.SourceStreamLastMessageOriginatingTime != null ? DateTimeHelper.FormatDateTime(this.SourceStreamLastMessageOriginatingTime) : "N/A"; + + /// + /// Gets the creation time of the last message in the source stream. + /// + [Browsable(false)] + public DateTime? SourceStreamLastMessageCreationTime + => this.SourceStreamMetadata != null && this.SourceStreamMetadata.LastMessageCreationTime != DateTime.MaxValue ? + this.SourceStreamMetadata?.LastMessageCreationTime : null; /// - /// Gets a string representation of the time of the last message in the stream. + /// Gets a string representation of the time of the last message in the source stream. /// [DisplayName("Source Stream Last Message CreationTime")] [Description("The creation time of the last message in the stream.")] - public virtual string SourceStreamLastMessageCreationTimeString - => DateTimeFormatHelper.FormatDateTime(this.SourceStreamLastMessageCreationTime); + public string SourceStreamLastMessageCreationTimeDisplayString + => this.SourceStreamLastMessageCreationTime != null ? DateTimeHelper.FormatDateTime(this.SourceStreamLastMessageCreationTime) : "N/A"; - /// - public override string DisplayName => this.IsDirty ? $"{this.Name}*" : this.Name; + /// + /// Gets the number of messages in the source stream. + /// + [Browsable(false)] + public long? SourceStreamMessageCount => this.SourceStreamMetadata?.MessageCount; - /// - public override string IconSource + /// + /// Gets a string representation of the number of messages in the source stream. + /// + [DisplayName("Source Stream Message Count")] + [Description("The total number of messages in the stream.")] + public string SourceStreamMessageCountDisplayString => + this.SourceStreamMessageCount != null ? this.SourceStreamMessageCount.ToString() : "N/A"; + + /// + /// Gets the total size of the messages in the source stream. + /// + [Browsable(false)] + public long? SourceStreamSize { get { - if (VisualizationContext.Instance.GetDataType(this.DataTypeName) == typeof(AudioBuffer)) - { - // Audio stream - return this.PartitionViewModel.IsLivePartition ? IconSourcePath.StreamAudioMutedLive : IconSourcePath.StreamAudioMuted; - } - else if (VisualizationContext.Instance.GetDataType(this.DataTypeName)?.Name == nameof(TimeIntervalAnnotation)) + if (this.SourceStreamMetadata is PsiStreamMetadata psiStreamMetadata) { - // Annotation - return IconSourcePath.Annotation; + return psiStreamMetadata.MessageSizeCumulativeSum; } - else if (this.InternalChildren.Any()) + else if (this.SourceStreamMetadata != null) { - // Group node that's also a stream - return this.PartitionViewModel.IsLivePartition ? IconSourcePath.StreamGroupLive : IconSourcePath.StreamGroup; + return (long)(this.SourceStreamMetadata.AverageMessageSize * this.SourceStreamMetadata.MessageCount); } else { - // Stream - return this.PartitionViewModel.IsLivePartition ? IconSourcePath.StreamLive : IconSourcePath.Stream; + return null; } } } /// - /// Ensures that a derived stream exists as a child of this stream tree node. + /// Gets a string representation of the total size of the messages in the source stream. + /// + [DisplayName("Source Stream Size")] + [Description("The size (in bytes) of data in the stream.")] + public string SourceStreamSizeDisplayString => + this.SourceStreamSize != null ? this.SourceStreamSize.ToString() : "N/A"; + + /// + /// Gets the average message size in the source stream. /// - /// The stream binding for the derived stream. - public virtual void EnsureDerivedStreamExists(StreamBinding streamBinding) + [Browsable(false)] + public double? SourceStreamAverageMessageSize { - var memberPath = default(string); - if (streamBinding.NodePath.StartsWith(streamBinding.StreamName)) + get { - memberPath = streamBinding.NodePath.Substring(streamBinding.StreamName.Length + 1); - } - else - { - throw new Exception("Unexpected derived stream binding."); + if (this.SourceStreamMetadata is PsiStreamMetadata psiStreamMetadata) + { + return this.SourceStreamMessageCount > 0 ? psiStreamMetadata.MessageSizeCumulativeSum / this.SourceStreamMessageCount : null; + } + else if (this.SourceStreamMetadata != null) + { + return this.SourceStreamMessageCount > 0 ? this.SourceStreamMetadata.AverageMessageSize : null; + } + else + { + return null; + } } + } + + /// + /// Gets a string representation of the average message size in the source stream. + /// + [DisplayName("Source Stream Avg. Message Size")] + [Description("The average size (in bytes) of messages in the source stream.")] + public string SourceStreamAverageMessageSizeDisplayString + => this.SourceStreamAverageMessageSize != null ? this.SourceStreamAverageMessageSize.ToString() : "N/A"; + + /// + /// Gets the average latency of messages in the source stream. + /// + [Browsable(false)] + public double? SourceStreamAverageMessageLatencyMs + => (this.SourceStreamMetadata != null && this.SourceStreamMessageCount > 0) ? this.SourceStreamMetadata.AverageMessageLatencyMs : null; + + /// + /// Gets a string representation of the average latency of messages in the source stream. + /// + [DisplayName("Source Stream Avg. Message Latency (ms)")] + [Description("The average latency (in milliseconds) of messages in the source stream.")] + public string SourceStreamAverageMessageLatencyMsDisplayString + => this.SourceStreamAverageMessageLatencyMs != null ? this.SourceStreamAverageMessageLatencyMs.ToString() : "N/A"; + + /// + /// Gets the opened time for the stream(s) subsumed by the node. + /// + [Browsable(false)] + public DateTime? SubsumedOpenedTime + => DateTimeHelper.MinDateTime( + this.SourceStreamOpenedTime, + DateTimeHelper.MinDateTime(this.Children.Where(c => c.IsSourceStream || c.HasSourceStreamDescendants).Select(c => c.SubsumedOpenedTime))); + + /// + /// Gets a string representation of the first opened time for the stream(s) subsumed by the node. + /// + [DisplayName("Subsumed OpenedTime")] + [Description("The first opened time for the stream(s) subsumed by the node.")] + public string SubsumedOpenedTimeDisplayString + => this.SubsumedOpenedTime != null ? DateTimeHelper.FormatDateTime(this.SubsumedOpenedTime) : "N/A"; + + /// + /// Gets the last closed time for the stream(s) subsumed by the node. + /// + [Browsable(false)] + public DateTime? SubsumedClosedTime + => DateTimeHelper.MaxDateTime( + this.SourceStreamClosedTime, + DateTimeHelper.MaxDateTime(this.Children.Where(c => c.IsSourceStream || c.HasSourceStreamDescendants).Select(c => c.SubsumedClosedTime))); + + /// + /// Gets a string representation of the last closed time for the stream(s) subsumed by the node. + /// + [DisplayName("Subsumed ClosedTime")] + [Description("The last closed time for the stream(s) subsumed by the node.")] + public string SubsumedClosedTimeDisplayString + => this.SubsumedClosedTime != null ? DateTimeHelper.FormatDateTime(this.SubsumedClosedTime) : "N/A"; + + /// + /// Gets the originating time of the first message in the stream(s) subsumed by the node. + /// + [Browsable(false)] + public DateTime? SubsumedFirstMessageOriginatingTime + => DateTimeHelper.MinDateTime( + this.SourceStreamFirstMessageOriginatingTime, + DateTimeHelper.MinDateTime(this.Children.Where(c => c.IsSourceStream || c.HasSourceStreamDescendants).Select(c => c.SubsumedFirstMessageOriginatingTime))); + + /// + /// Gets a string representation of the originating time of the first message in the stream(s) subsumed by the node. + /// + [DisplayName("Subsumed First Message OriginatingTime")] + [Description("The originating time of the first message in the stream(s) subsumed by the node.")] + public string SubsumedFirstMessageOriginatingTimeDisplayString + => this.SubsumedFirstMessageOriginatingTime != null ? DateTimeHelper.FormatDateTime(this.SubsumedFirstMessageOriginatingTime) : "N/A"; + + /// + /// Gets the creation time of the first message in the stream(s) subsumed by the node. + /// + [Browsable(false)] + public DateTime? SubsumedFirstMessageCreationTime + => DateTimeHelper.MinDateTime( + this.SourceStreamFirstMessageCreationTime, + DateTimeHelper.MinDateTime(this.Children.Where(c => c.IsSourceStream || c.HasSourceStreamDescendants).Select(c => c.SubsumedFirstMessageCreationTime))); + + /// + /// Gets a string representation of the time of the first message in the stream(s) subsumed by the node. + /// + [DisplayName("Subsumed First Message CreationTime")] + [Description("The creation time of the first message in the stream(s) subsumed by the node.")] + public string SubsumedFirstMessageCreationTimeDisplayString + => this.SubsumedFirstMessageCreationTime != null ? DateTimeHelper.FormatDateTime(this.SubsumedFirstMessageCreationTime) : "N/A"; + + /// + /// Gets the originating time of the last message in the stream(s) subsumed by the node. + /// + [Browsable(false)] + public DateTime? SubsumedLastMessageOriginatingTime + => DateTimeHelper.MaxDateTime( + this.SourceStreamLastMessageOriginatingTime, + DateTimeHelper.MaxDateTime(this.Children.Where(c => c.IsSourceStream || c.HasSourceStreamDescendants).Select(c => c.SubsumedLastMessageOriginatingTime))); + + /// + /// Gets a string representation of the originating time of the last message in the stream(s) subsumed by the node. + /// + [DisplayName("Subsumed Last Message OriginatingTime")] + [Description("The originating time of the last message in the stream(s) subsumed by the node.")] + public string SubsumedLastMessageOriginatingTimeDisplayString + => this.SubsumedLastMessageOriginatingTime != null ? DateTimeHelper.FormatDateTime(this.SubsumedLastMessageOriginatingTime) : "N/A"; + + /// + /// Gets the creation time of the last message in the stream(s) subsumed by the node. + /// + [Browsable(false)] + public DateTime? SubsumedLastMessageCreationTime + => DateTimeHelper.MaxDateTime( + this.SourceStreamLastMessageCreationTime, + DateTimeHelper.MaxDateTime(this.Children.Where(c => c.IsSourceStream || c.HasSourceStreamDescendants).Select(c => c.SubsumedLastMessageCreationTime))); + + /// + /// Gets a string representation of the time of the last message in the stream(s) subsumed by the node. + /// + [DisplayName("Subsumed Last Message CreationTime")] + [Description("The creation time of the last message in the stream(s) subsumed by the node.")] + public string SubsumedLastMessageCreationTimeDisplayString + => this.SubsumedLastMessageCreationTime != null ? DateTimeHelper.FormatDateTime(this.SubsumedLastMessageCreationTime) : "N/A"; + + /// + /// Gets the total number of messages in the stream(s) subsumed by the node. + /// + [Browsable(false)] + public long? SubsumedMessageCount => + SizeHelper.NullableSum( + this.SourceStreamMessageCount, + SizeHelper.NullableSum(this.Children.Where(c => c.IsSourceStream || c.HasSourceStreamDescendants).Select(c => c.SubsumedMessageCount))); + + /// + /// Gets a string representation of the total number of messages in the stream(s) subsumed by the node. + /// + [DisplayName("Subsumed Message Count")] + [Description("The total number of messages in the stream(s) subsumed by the node.")] + public string SubsumedMessageCountDisplayString + => this.SubsumedMessageCount != null ? this.SubsumedMessageCount.ToString() : "N/A"; + + /// + /// Gets the average message latency for the stream(s) subsumed by the node. + /// + [Browsable(false)] + public double? SubsumedAverageMessageLatencyMs + => this.HasSourceStreamDescendants ? + (this.SubsumedMessageCount != null && this.SubsumedMessageCount > 0) ? + ((this.Children.Where(c => c.IsSourceStream || c.HasSourceStreamDescendants).Sum(c => c.SubsumedMessageCount == null || c.SubsumedMessageCount == 0 ? 0 : (c.SubsumedAverageMessageLatencyMs * c.SubsumedMessageCount)) + + (this.SourceStreamMessageCount == null || this.SourceStreamMessageCount == 0 ? 0 : (this.SourceStreamAverageMessageLatencyMs * this.SourceStreamMessageCount))) / + this.SubsumedMessageCount) + : default + : this.SourceStreamAverageMessageLatencyMs; + + /// + /// Gets a string representation of the average message latency for the stream(s) subsumed by the node. + /// + [DisplayName("Subsumed Avg. Message Latency (ms)")] + [Description("The average latency (in milliseconds) of messages in the stream(s) subsumed by the node.")] + public string SubsumedAverageMessageLatencyMsDisplayString + => this.SubsumedAverageMessageLatencyMs != null ? this.SubsumedAverageMessageLatencyMs.ToString() : "N/A"; + + /// + /// Gets the average message size for the stream(s) subsumed by the node. + /// + [Browsable(false)] + public double? SubsumedAverageMessageSize + => this.HasSourceStreamDescendants ? + (this.SubsumedMessageCount != null && this.SubsumedMessageCount > 0) ? + (this.Children.Where(c => c.IsSourceStream || c.HasSourceStreamDescendants).Sum(c => c.SubsumedMessageCount == null || c.SubsumedMessageCount == 0 ? 0 : (c.SubsumedAverageMessageSize * c.SubsumedMessageCount)) + + (this.SourceStreamMessageCount == null || this.SourceStreamMessageCount == 0 ? 0 : (this.SourceStreamAverageMessageSize * this.SourceStreamMessageCount))) / + this.SubsumedMessageCount + : default + : this.SourceStreamAverageMessageSize; + + /// + /// Gets a string representation of the average message size for the stream(s) subsumed by the node. + /// + [DisplayName("Subsumed Avg. Message Size")] + [Description("The average size (in bytes) of messages in the stream(s) subsumed by the node.")] + public string SubsumedAverageMessageSizeDisplayString + => this.SubsumedAverageMessageSize != null ? this.SubsumedAverageMessageSize.ToString() : "N/A"; + + /// + /// Gets the total data size in the stream(s) subsumed by the node. + /// + [Browsable(false)] + public long? SubsumedSize + => SizeHelper.NullableSum( + this.SourceStreamSize, + SizeHelper.NullableSum(this.Children.Where(c => c.IsSourceStream || c.HasSourceStreamDescendants).Select(c => c.SubsumedSize))); + + /// + /// Gets a string representation of the total data size in the stream(s) subsumed by the node. + /// + [DisplayName("Subsumed Size")] + [Description("The size (in bytes) of data in the stream(s) subsumed by the node.")] + public string SubsumedSizeDisplayString + => this.SubsumedSize != null ? this.SubsumedSize.ToString() : "N/A"; - if (this.FindStreamTreeNode(memberPath) == null) + /// + /// Gets or sets a value indicating whether the stream has unsaved changes. + /// + [Browsable(false)] + public bool IsDirty + { + get => this.isDirty; + set { - this.AddDerivedMemberStreamChildren(); - this.ExpandAll(); + this.RaisePropertyChanging(nameof(this.DisplayString)); + this.isDirty = value; + this.RaisePropertyChanged(nameof(this.DisplayString)); } + } - if (memberPath.Contains('.')) + /// + /// Gets a value indicating whether the stream's supplemental metadata (if any) is known, readable type. + /// + [Browsable(false)] + public bool SupplementalMetadataTypeIsKnown + { + get { - var pathItems = memberPath.Split('.'); - if (this.InternalChildren.FirstOrDefault(p => p.Name == pathItems.First()) is DerivedMemberStreamTreeNode memberChild) + // If there is no supplemental metadata type for the stream + if (string.IsNullOrWhiteSpace(this.SourceStreamMetadata?.SupplementalMetadataTypeName)) { - memberChild.EnsureDerivedStreamExists(streamBinding); + // Then return false + return false; + } + + // If we've not tried to read the supplemental metadata yet, do so now. + if (!this.supplementalMetadataTypeIsKnown.HasValue) + { + try + { + // Attempt to read the supplemental metadata for the stream. + MethodInfo methodInfo = DataManager.Instance.GetType().GetMethod( + nameof(DataManager.GetSupplementalMetadataByName), + new Type[] { typeof(string), typeof(string), typeof(Type), typeof(string) }); + + MethodInfo genericMethodInfo = methodInfo.MakeGenericMethod(TypeResolutionHelper.GetVerifiedType(this.SourceStreamMetadata.SupplementalMetadataTypeName)); + + var parameters = new object[] + { + this.PartitionViewModel.StoreName, + this.PartitionViewModel.StorePath, + TypeResolutionHelper.GetVerifiedType(this.PartitionViewModel.Partition.StreamReaderTypeName), + this.SourceStreamName, + }; + genericMethodInfo.Invoke(DataManager.Instance, parameters); + + // Success + this.supplementalMetadataTypeIsKnown = true; + } + catch (Exception) + { + this.supplementalMetadataTypeIsKnown = false; + } } + + return this.supplementalMetadataTypeIsKnown.Value; } } + /// + /// Gets the command that executes when opening the stream tree node context menu. + /// + [Browsable(false)] + public RelayCommand ContextMenuOpeningCommand + => this.contextMenuOpeningCommand ??= new RelayCommand( + grid => + { + var contextMenu = new ContextMenu(); + this.PopulateContextMenu(contextMenu); + grid.ContextMenu = contextMenu; + }); + + /// + /// Gets the command that executes when double clicking on the stream tree node. + /// + [Browsable(false)] + public RelayCommand MouseDoubleClickCommand + => this.mouseDoubleClickCommand ??= new RelayCommand(e => this.OnMouseDoubleClick(e)); + + /// + /// Creates the root stream tree node for a specified partition. + /// + /// The partition view model. + /// A root stream tree node for the specified partition. + public static StreamTreeNode CreateRoot(PartitionViewModel partitionViewModel) + => new (partitionViewModel, null, null, null, null); + + /// + /// Creates a stream binding for this stream tree node and a specified visualizer. + /// + /// The visualizer to create a stream binding for. + /// A corresponding stream binding. + public StreamBinding CreateStreamBinding(VisualizerMetadata visualizerMetadata) + => new ( + this.SourceStreamMetadata.Name, + this.PartitionViewModel.Name, + this.FullName, + this.DerivedStreamAdapterType, + this.DerivedStreamAdapterArguments, + visualizerMetadata.StreamAdapterType, + visualizerMetadata.StreamAdapterArguments, + visualizerMetadata.SummarizerType, + visualizerMetadata.SummarizerArguments); + /// /// Selects the list of visualizers compatible with this stream tree node. /// @@ -423,22 +852,21 @@ public virtual void EnsureDerivedStreamExists(StreamBinding streamBinding) /// A nullable boolean indicating constraints on whether the visualizer should be a universal one (visualize messages, visualize latency etc). /// A nullable boolean indicating constraints on whether the visualizer should be a "in new panel" one. /// The matching list of visualizers. - public virtual List GetCompatibleVisualizers( + public List GetCompatibleVisualizers( VisualizationPanel visualizationPanel = null, bool? isUniversal = null, bool? isInNewPanel = null) { var results = new List(); - var nodeDataType = VisualizationContext.Instance.GetDataType(this.DataTypeName); - var comparer = new VisualizerMetadataComparer(nodeDataType); + var comparer = new VisualizerMetadataComparer(this.DataType); // If we're looking for visualizers that fit in any panel if (visualizationPanel == null) { results.AddRange(VisualizationContext.Instance.PluginMap.Visualizers.Where(v => - (nodeDataType == v.DataType || - nodeDataType.IsSubclassOf(v.DataType) || - (v.DataType.IsInterface && v.DataType.IsAssignableFrom(nodeDataType))) && + (this.DataType == v.DataType || + this.DataType.IsSubclassOf(v.DataType) || + (v.DataType.IsInterface && v.DataType.IsAssignableFrom(this.DataType))) && (!isInNewPanel.HasValue || v.IsInNewPanel == isInNewPanel.Value) && (!isUniversal.HasValue || v.IsUniversalVisualizer == isUniversal))); } @@ -447,9 +875,9 @@ public virtual void EnsureDerivedStreamExists(StreamBinding streamBinding) // o/w find out the compatible panel types results.AddRange(VisualizationContext.Instance.PluginMap.Visualizers.Where(v => visualizationPanel.CompatiblePanelTypes.Contains(v.VisualizationPanelType) && - (nodeDataType == v.DataType || - nodeDataType.IsSubclassOf(v.DataType) || - (v.DataType.IsInterface && v.DataType.IsAssignableFrom(nodeDataType))) && + (this.DataType == v.DataType || + this.DataType.IsSubclassOf(v.DataType) || + (v.DataType.IsInterface && v.DataType.IsAssignableFrom(this.DataType))) && (!isInNewPanel.HasValue || v.IsInNewPanel == isInNewPanel.Value) && (!isUniversal.HasValue || v.IsUniversalVisualizer == isUniversal))); } @@ -459,9 +887,9 @@ public virtual void EnsureDerivedStreamExists(StreamBinding streamBinding) if ((!isUniversal.HasValue || !isUniversal.Value) && (visualizationPanel == null || visualizationPanel.CompatiblePanelTypes.Contains(VisualizationPanelType.Timeline))) { - if (nodeDataType.IsGenericType && nodeDataType.GetGenericTypeDefinition() == typeof(Dictionary<,>)) + if (this.DataType.IsGenericType && this.DataType.GetGenericTypeDefinition() == typeof(Dictionary<,>)) { - var genericArguments = nodeDataType.GetGenericArguments(); + var genericArguments = this.DataType.GetGenericArguments(); var numericSeriesVisualizationObjectType = NumericSeriesVisualizationObject.GetSeriesVisualizationObjectTypeByNumericType(genericArguments[1]); if (numericSeriesVisualizationObjectType != null) @@ -512,155 +940,374 @@ public virtual void EnsureDerivedStreamExists(StreamBinding streamBinding) } /// - /// Creates a stream binding for this stream tree node and a specified visualizer. - /// - /// The visualizer to create a stream binding for. - /// A corresponding stream binding. - public virtual StreamBinding CreateStreamBinding(VisualizerMetadata visualizerMetadata) - => new ( - this.SourceStreamMetadata.Name, - this.PartitionViewModel.Name, - this.Path, - visualizerMetadata.StreamAdapterType, - null, - visualizerMetadata.SummarizerType, - null, - false); - - /// - /// Customizes a list of visualizers by inserting custom adapters where necessary. + /// Adds a new stream tree node as child of this node. /// - /// The list of visualizers. - protected virtual void InsertCustomAdapters(List metadatas) + /// The child name, relative to this node. + /// The source stream metadata. + /// The derived stream adapter type. + /// The derived stream adapter arguments. + /// Whether to add the new node in sorted order. + /// A reference to the child stream tree node if successfully created, or null otherwise. + public StreamTreeNode AddChild( + string childName, + IStreamMetadata sourceStreamMetadata, + Type derivedStreamAdapterType, + object[] derivedStreamAdapterArguments, + bool sorted = false) { - var streamSourceDataType = VisualizationContext.Instance.GetDataType(this.SourceStreamMetadata.TypeName); + // Add the child node by recursing over the child name items + var streamTreeNode = this.AddChild(childName.Split('.'), sourceStreamMetadata, derivedStreamAdapterType, derivedStreamAdapterArguments, sorted); - // For each of the non-universal visualization objects, add a data adapter from the stream data type to the subfield data type - for (int index = 0; index < metadatas.Count; index++) + // If we have sucessfully added a child that corresponds to a source stream + if (this.FullName != null && streamTreeNode != null && streamTreeNode.IsSourceStream) { - // For message visualization object insert a custom object adapter so values can be displayed for known types. - if (metadatas[index].VisualizationObjectType == typeof(MessageVisualizationObject)) + // Then mark to the root that we have stream descendants + var fullNamePathItems = this.FullName.Split('.'); + for (int i = 1; i <= fullNamePathItems.Length; i++) { - var objectAdapterType = typeof(ObjectAdapter<>).MakeGenericType(streamSourceDataType); - metadatas[index] = metadatas[index].GetCloneWithNewStreamAdapterType(objectAdapterType); - } - else if (metadatas[index].VisualizationObjectType == typeof(LatencyVisualizationObject)) - { - // o/w for latency visualization object insert a custom object adapter so values can be displayed for known types. - var objectToLatencyAdapterType = typeof(ObjectToLatencyAdapter<>).MakeGenericType(streamSourceDataType); - metadatas[index] = metadatas[index].GetCloneWithNewStreamAdapterType(objectToLatencyAdapterType); - } - else if (metadatas[index].DataType.IsInterface) - { - // o/w for interface types inject an interface adapter - var interfaceAdapterType = typeof(InterfaceAdapter<,>).MakeGenericType(streamSourceDataType, metadatas[index].DataType); - metadatas[index] = metadatas[index].GetCloneWithNewStreamAdapterType(interfaceAdapterType); + var ancestorName = fullNamePathItems.Take(i).EnumerableToString("."); + this.FindChild(ancestorName).HasSourceStreamDescendants = true; } } + + return streamTreeNode; } /// - /// Adds all derived member stream children. + /// Selects a child tree node and expands all nodes on the path to it. /// - protected virtual void AddDerivedMemberStreamChildren() - { - // Get the type of this node. - Type dataType = TypeResolutionHelper.GetVerifiedType(this.DataTypeName); + /// The child name, relative to this node. + /// True if the child was found, otherwise false. + public bool SelectChild(string childName) => this.SelectChild(childName.Split('.')); - if (dataType != null) - { - // Determine if the current node is a reference type - var isReference = !dataType.IsValueType || Nullable.GetUnderlyingType(dataType) != null; - - // Create a child node for each public instance property that takes no parameters. - foreach (PropertyInfo propertyInfo in dataType.GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(property => !property.GetMethod.GetParameters().Any())) - { - this.AddDerivedMemberStreamChild(propertyInfo, propertyInfo.PropertyType, isReference && propertyInfo.PropertyType.IsValueType); - } + /// + /// Finds a child tree node by name. + /// + /// The child name, relative to this node. + /// The stream tree node corresponding to the child if found, or null otherwise. + public StreamTreeNode FindChild(string childName) => this.FindChild(childName.Split('.')); - // Create a child node for each public instance field - foreach (FieldInfo fieldInfo in dataType.GetFields(BindingFlags.Public | BindingFlags.Instance)) - { - this.AddDerivedMemberStreamChild(fieldInfo, fieldInfo.FieldType, isReference && fieldInfo.FieldType.IsValueType); - } + /// + /// Expands this node and all of its child nodes recursively. + /// + public void ExpandAll() + { + foreach (var child in this.Children) + { + child.ExpandAll(); } + + this.IsTreeNodeExpanded = true; } /// - /// Adds a derived member stream child. + /// Collapses this node and all of its child nodes recursively. /// - /// The member info. - /// The member type. - /// Indicates whether we should do a nullable expansion. - protected void AddDerivedMemberStreamChild(MemberInfo memberInfo, Type memberType, bool generateNullable) + public void CollapseAll() { - var child = new DerivedMemberStreamTreeNode(this, memberInfo, memberType, generateNullable); + this.IsTreeNodeExpanded = false; - // Insert the child into the existing list, before all non-member sub-streams, and in alphabetical order - var lastOrDefault = this.InternalChildren.LastOrDefault(stn => string.Compare(stn.Name, memberInfo.Name) < 0 && stn is StreamTreeNode); - var index = lastOrDefault != null ? this.InternalChildren.IndexOf(lastOrDefault) + 1 : 0; - this.InternalChildren.Insert(index, child); + foreach (var child in this.Children) + { + child.CollapseAll(); + } } /// - protected override void PopulateContextMenu(ContextMenu contextMenu) - { - this.PopulateContextMenuWithVisualizers(contextMenu); - this.PopulateContextMenuWithExpandMembers(contextMenu); - this.PopulateContextMenuWithZoomToStream(contextMenu); + public override string ToString() => $"Node: {this.Name}"; - base.PopulateContextMenu(contextMenu); + /// + /// Event handler for partition property changed. + /// + /// The sender. + /// The event arguments. + private void OnPartitionViewModelPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(this.PartitionViewModel.IsLivePartition)) + { + this.RaisePropertyChanged(nameof(this.IconSource)); + } } - /// - protected override void OnMouseDoubleClick(MouseButtonEventArgs e) + /// + /// Event handler for dataset view model property changed. + /// + /// The sender. + /// The event arguments. + private void OnDatasetViewModelPropertyChanged(object sender, PropertyChangedEventArgs e) { - base.OnMouseDoubleClick(e); - - if (this.CanExpandDerivedMemberStreams()) + if (e.PropertyName == nameof(this.DatasetViewModel.CurrentSessionViewModel)) { - this.AddDerivedMemberStreamChildren(); - this.ExpandAll(); - this.IsTreeNodeExpanded = true; - e.Handled = true; + this.RaisePropertyChanged(nameof(this.UiElementOpacity)); + this.RaisePropertyChanged(nameof(this.IsInCurrentSession)); + } + else if (e.PropertyName == nameof(this.DatasetViewModel.ShowAuxiliaryStreamInfo)) + { + this.UpdateAuxiliaryInfo(); } } /// - /// Populates a specified context menu with visualizers. + /// Handler for a double-click event on the stream tree node. /// - /// The context menu to populate. - protected void PopulateContextMenuWithVisualizers(ContextMenu contextMenu) + /// The mouse button event arguments. + private void OnMouseDoubleClick(MouseButtonEventArgs e) { - var currentPanel = VisualizationContext.Instance.VisualizationContainer.CurrentPanel; - - // sectionStart and sectionEnd keep track of how many items were added in each - // section of the menu, and can be used to reason about when to add a separator - var previousIconPath = default(string); - - // get the type-specific visualizers that work in the current panel - if (currentPanel != null) + if (this.IsStream && this.CanAddMemberDerivedStreams()) { - var specificInCurrentPanel = this.GetCompatibleVisualizers(visualizationPanel: currentPanel, isUniversal: false, isInNewPanel: false); - if (specificInCurrentPanel.Any() && contextMenu.Items.Count > 0) - { - contextMenu.Items.Add(new Separator()); - } + this.AddMemberDerivedStreams(out var membersNotAdded); + this.ExpandAll(); + this.IsTreeNodeExpanded = true; + e.Handled = true; - foreach (var visualizer in specificInCurrentPanel) + if (membersNotAdded.Any()) { - // Create and add a menu item. Currently show icon only if different from previous - contextMenu.Items.Add(this.CreateVisualizeStreamMenuItem(visualizer, showIcon: visualizer.IconSourcePath != previousIconPath)); - previousIconPath = visualizer.IconSourcePath; + Application.Current.Dispatcher.BeginInvoke((Action)(() => + { + new MessageBoxWindow( + Application.Current.MainWindow, + "Warning", + $"The following member(s) were not added because children with the same names already exist: {membersNotAdded.EnumerableToString(", ")}.", + "Close", + null).ShowDialog(); + })); } } + } - // get the type specific visualizers in a new panel - previousIconPath = default; - var specificInNewPanel = this.GetCompatibleVisualizers(visualizationPanel: null, isUniversal: false, isInNewPanel: true); - - // add a separator if necessary - if (specificInNewPanel.Any() && contextMenu.Items.Count > 0) + /// + /// Updates the auxiliary info to be displayed. + /// + private void UpdateAuxiliaryInfo() + { + var indexedMarker = this.IsIndexedPsiStream ? "*" : string.Empty; + switch (this.PartitionViewModel.SessionViewModel.DatasetViewModel.ShowAuxiliaryStreamInfo) + { + case AuxiliaryStreamInfo.None: + this.AuxiliaryInfo = string.Empty; + break; + case AuxiliaryStreamInfo.Size: + this.AuxiliaryInfo = this.HasSourceStreamDescendants && this.SubsumedSize != null ? $"[{SizeHelper.FormatSize(this.SubsumedSize.Value)}] " : string.Empty; + if (this.IsStream) + { + this.AuxiliaryInfo += " " + indexedMarker + SizeHelper.FormatSize(this.SourceStreamSize.Value); + } + + break; + case AuxiliaryStreamInfo.DataThroughputPerHour: + this.AuxiliaryInfo = string.Empty; + + if (this.HasSourceStreamDescendants && this.SubsumedClosedTime != null && this.SubsumedOpenedTime != null) + { + var subsumedThroughput = this.SubsumedSize != null ? (this.SubsumedSize.Value / (this.SubsumedClosedTime.Value - this.SubsumedOpenedTime.Value).TotalHours) : 0; + this.AuxiliaryInfo += $"[{SizeHelper.FormatThroughput(subsumedThroughput, "hour")}]"; + } + + if (this.IsStream) + { + var throughput = this.SourceStreamSize != null ? (this.SourceStreamSize.Value / (this.SourceStreamClosedTime.Value - this.SourceStreamOpenedTime.Value).TotalHours) : 0; + this.AuxiliaryInfo += " " + indexedMarker + SizeHelper.FormatThroughput(throughput, "hour"); + } + + break; + case AuxiliaryStreamInfo.DataThroughputPerMinute: + this.AuxiliaryInfo = string.Empty; + + if (this.HasSourceStreamDescendants && this.SubsumedClosedTime != null && this.SubsumedOpenedTime != null) + { + var subsumedThroughput = this.SubsumedSize != null ? (this.SubsumedSize.Value / (this.SubsumedClosedTime.Value - this.SubsumedOpenedTime.Value).TotalMinutes) : 0; + this.AuxiliaryInfo += $"[{SizeHelper.FormatThroughput(subsumedThroughput, "min")}]"; + } + + if (this.IsStream) + { + var throughput = this.SourceStreamSize != null ? (this.SourceStreamSize.Value / (this.SourceStreamClosedTime.Value - this.SourceStreamOpenedTime.Value).TotalMinutes) : 0; + this.AuxiliaryInfo += " " + indexedMarker + SizeHelper.FormatThroughput(throughput, "min"); + } + + break; + case AuxiliaryStreamInfo.DataThroughputPerSecond: + this.AuxiliaryInfo = string.Empty; + + if (this.HasSourceStreamDescendants && this.SubsumedClosedTime != null && this.SubsumedOpenedTime != null) + { + var subsumedThroughput = this.SubsumedSize != null ? (this.SubsumedSize.Value / (this.SubsumedClosedTime.Value - this.SubsumedOpenedTime.Value).TotalSeconds) : 0; + this.AuxiliaryInfo += $"[{SizeHelper.FormatThroughput(subsumedThroughput, "sec")}]"; + } + + if (this.IsStream) + { + var throughput = this.SourceStreamSize != null ? (this.SourceStreamSize.Value / (this.SourceStreamClosedTime.Value - this.SourceStreamOpenedTime.Value).TotalSeconds) : 0; + this.AuxiliaryInfo += " " + indexedMarker + SizeHelper.FormatThroughput(throughput, "sec"); + } + + break; + case AuxiliaryStreamInfo.MessageCountThroughputPerHour: + this.AuxiliaryInfo = string.Empty; + + if (this.HasSourceStreamDescendants && this.SubsumedClosedTime != null && this.SubsumedOpenedTime != null) + { + var subsumedThroughput = this.SubsumedMessageCount != null ? (this.SubsumedMessageCount.Value / (this.SubsumedClosedTime.Value - this.SubsumedOpenedTime.Value).TotalHours) : 0; + this.AuxiliaryInfo += $"[{subsumedThroughput:0.01}]"; + } + + if (this.IsStream) + { + var throughput = this.SourceStreamMessageCount != null ? (this.SourceStreamMessageCount.Value / (this.SourceStreamClosedTime.Value - this.SourceStreamOpenedTime.Value).TotalHours) : 0; + this.AuxiliaryInfo += " " + indexedMarker + $"{throughput:0.01}"; + } + + break; + case AuxiliaryStreamInfo.MessageCountThroughputPerMinute: + this.AuxiliaryInfo = string.Empty; + + if (this.HasSourceStreamDescendants && this.SubsumedClosedTime != null && this.SubsumedOpenedTime != null) + { + var subsumedThroughput = this.SubsumedMessageCount != null ? (this.SubsumedMessageCount.Value / (this.SubsumedClosedTime.Value - this.SubsumedOpenedTime.Value).TotalMinutes) : 0; + this.AuxiliaryInfo += $"[{subsumedThroughput:0.01}]"; + } + + if (this.IsStream) + { + var throughput = this.SourceStreamMessageCount != null ? (this.SourceStreamMessageCount.Value / (this.SourceStreamClosedTime.Value - this.SourceStreamOpenedTime.Value).TotalMinutes) : 0; + this.AuxiliaryInfo += " " + indexedMarker + $"{throughput:0.01}"; + } + + break; + case AuxiliaryStreamInfo.MessageCountThroughputPerSecond: + this.AuxiliaryInfo = string.Empty; + + if (this.HasSourceStreamDescendants && this.SubsumedClosedTime != null && this.SubsumedOpenedTime != null) + { + var subsumedThroughput = this.SubsumedMessageCount != null ? (this.SubsumedMessageCount.Value / (this.SubsumedClosedTime.Value - this.SubsumedOpenedTime.Value).TotalSeconds) : 0; + this.AuxiliaryInfo += $"[{subsumedThroughput:0.01}]"; + } + + if (this.IsStream) + { + var throughput = this.SourceStreamMessageCount != null ? (this.SourceStreamMessageCount.Value / (this.SourceStreamClosedTime.Value - this.SourceStreamOpenedTime.Value).TotalSeconds) : 0; + this.AuxiliaryInfo += " " + indexedMarker + $"{throughput:0.01}"; + } + + break; + case AuxiliaryStreamInfo.MessageCount: + this.AuxiliaryInfo = string.Empty; + + if (this.HasSourceStreamDescendants && this.SubsumedMessageCount != null) + { + this.AuxiliaryInfo += $"[{this.SubsumedMessageCount:0,0}]"; + } + + if (this.IsStream) + { + this.AuxiliaryInfo += $" {this.SourceStreamMessageCount:0,0}"; + } + + break; + case AuxiliaryStreamInfo.AverageMessageLatencyMs: + this.AuxiliaryInfo = string.Empty; + + if (this.HasSourceStreamDescendants && this.SubsumedAverageMessageLatencyMs != null) + { + this.AuxiliaryInfo += $"[{SizeHelper.FormatLatencyMs(this.SubsumedAverageMessageLatencyMs.Value)}]"; + } + + if (this.IsStream && this.SourceStreamAverageMessageLatencyMs != null) + { + this.AuxiliaryInfo += SizeHelper.FormatLatencyMs(this.SourceStreamAverageMessageLatencyMs.Value); + } + + break; + + case AuxiliaryStreamInfo.AverageMessageSize: + this.AuxiliaryInfo = string.Empty; + + if (this.HasSourceStreamDescendants && this.SubsumedAverageMessageSize != null) + { + this.AuxiliaryInfo += $"[{SizeHelper.FormatSize(this.SubsumedAverageMessageSize.Value)}]"; + } + + if (this.IsStream && this.SourceStreamAverageMessageSize != null) + { + this.AuxiliaryInfo += indexedMarker + SizeHelper.FormatSize(this.SourceStreamAverageMessageSize.Value); + } + + break; + default: + break; + } + } + + /// + /// Populates a context menu with items for this node. + /// + /// The context menu to populate. + private void PopulateContextMenu(ContextMenu contextMenu) + { + if (this.IsStream) + { + this.PopulateContextMenuWithVisualizers(contextMenu); + this.PopulateContextMenuWithModifyScript(contextMenu); + this.PopulateContextMenuWithDerivedStreamExpansions(contextMenu); + } + + this.PopulateContextMenuWithCopyToClipboard(contextMenu); + + if (this.IsStream) + { + this.PopulateContextMenuWithZoomToStream(contextMenu); + } + + // Add the visualize session context menu if the stream is not in the currently visualized session + if (!this.IsInCurrentSession) + { + if (contextMenu.Items.Count > 0) + { + contextMenu.Items.Add(new Separator()); + } + + contextMenu.Items.Add(MenuItemHelper.CreateMenuItem(string.Empty, ContextMenuName.VisualizeSession, this.SessionViewModel.VisualizeSessionCommand)); + } + + this.PopulateContextMenuWithExpandAndCollapseAll(contextMenu); + this.PopulateContextMenuWithShowStreamInfo(contextMenu); + } + + /// + /// Populates a specified context menu with visualizers. + /// + /// The context menu to populate. + private void PopulateContextMenuWithVisualizers(ContextMenu contextMenu) + { + var currentPanel = VisualizationContext.Instance.VisualizationContainer.CurrentPanel; + + // sectionStart and sectionEnd keep track of how many items were added in each + // section of the menu, and can be used to reason about when to add a separator + var previousIconPath = default(string); + + // get the type-specific visualizers that work in the current panel + if (currentPanel != null) + { + var specificInCurrentPanel = this.GetCompatibleVisualizers(visualizationPanel: currentPanel, isUniversal: false, isInNewPanel: false); + if (specificInCurrentPanel.Any() && contextMenu.Items.Count > 0) + { + contextMenu.Items.Add(new Separator()); + } + + foreach (var visualizer in specificInCurrentPanel) + { + // Create and add a menu item. Currently show icon only if different from previous + contextMenu.Items.Add(this.CreateVisualizeStreamMenuItem(visualizer, showIcon: visualizer.IconSourcePath != previousIconPath)); + previousIconPath = visualizer.IconSourcePath; + } + } + + // get the type specific visualizers in a new panel + previousIconPath = default; + var specificInNewPanel = this.GetCompatibleVisualizers(visualizationPanel: null, isUniversal: false, isInNewPanel: true); + + // add a separator if necessary + if (specificInNewPanel.Any() && contextMenu.Items.Count > 0) { contextMenu.Items.Add(new Separator()); } @@ -707,57 +1354,257 @@ protected void PopulateContextMenuWithVisualizers(ContextMenu contextMenu) } /// - /// Populates a specified context menu with expand members command. + /// Populates a specified context menu with stream expansion commands. /// /// The context menu to populate. - protected void PopulateContextMenuWithExpandMembers(ContextMenu contextMenu) + private void PopulateContextMenuWithDerivedStreamExpansions(ContextMenu contextMenu) + { + if (contextMenu.Items.Count > 0) + { + contextMenu.Items.Add(new Separator()); + } + + this.PopulateContextMenuWithAddMemberDerivedStreams(contextMenu); + this.PopulateContextMenuWithAddDictionaryKeyDerivedStreams(contextMenu); + this.PopulateContextMenuWithAddScriptDerivedStream(contextMenu); + } + + private void PopulateContextMenuWithCopyToClipboard(ContextMenu contextMenu) { if (contextMenu.Items.Count > 0) { contextMenu.Items.Add(new Separator()); } + // Add copy to clipboard menu with sub-menu items + var copyToClipboardMenuItem = MenuItemHelper.CreateMenuItem( + string.Empty, + "Copy to Clipboard", + null); + + copyToClipboardMenuItem.Items.Add( + MenuItemHelper.CreateMenuItem( + null, + "Session Name", + VisualizationContext.Instance.VisualizationContainer.Navigator.CopyToClipboardCommand, + null, + true, + this.SessionViewModel.Name)); + + copyToClipboardMenuItem.Items.Add( + MenuItemHelper.CreateMenuItem( + null, + "Partition Name", + VisualizationContext.Instance.VisualizationContainer.Navigator.CopyToClipboardCommand, + null, + true, + this.PartitionViewModel.Name)); + + copyToClipboardMenuItem.Items.Add( + MenuItemHelper.CreateMenuItem( + null, + "Partition Store Name", + VisualizationContext.Instance.VisualizationContainer.Navigator.CopyToClipboardCommand, + null, + true, + this.PartitionViewModel.StoreName)); + + copyToClipboardMenuItem.Items.Add( + MenuItemHelper.CreateMenuItem( + null, + "Partition Store Path", + VisualizationContext.Instance.VisualizationContainer.Navigator.CopyToClipboardCommand, + null, + true, + this.PartitionViewModel.StorePath)); + + if (this.IsStream) + { + copyToClipboardMenuItem.Items.Add(new Separator()); + + copyToClipboardMenuItem.Items.Add( + MenuItemHelper.CreateMenuItem( + null, + "Stream Name", + VisualizationContext.Instance.VisualizationContainer.Navigator.CopyToClipboardCommand, + null, + true, + this.FullName)); + + copyToClipboardMenuItem.Items.Add( + MenuItemHelper.CreateMenuItem( + null, + "Source Stream Name", + VisualizationContext.Instance.VisualizationContainer.Navigator.CopyToClipboardCommand, + null, + true, + this.SourceStreamName)); + + copyToClipboardMenuItem.Items.Add( + MenuItemHelper.CreateMenuItem( + null, + "Source Stream Type", + VisualizationContext.Instance.VisualizationContainer.Navigator.CopyToClipboardCommand, + null, + true, + this.SourceStreamTypeDisplayString)); + + copyToClipboardMenuItem.Items.Add( + MenuItemHelper.CreateMenuItem( + null, + "Source Stream Type (Assembly Qualified)", + VisualizationContext.Instance.VisualizationContainer.Navigator.CopyToClipboardCommand, + null, + true, + this.SourceStreamTypeFullNameDisplayString)); + } + + contextMenu.Items.Add(copyToClipboardMenuItem); + } + + /// + /// Populates a specified context menu with the Add Members Derived Streams command. + /// + /// The context menu to populate. + private void PopulateContextMenuWithAddMemberDerivedStreams(ContextMenu contextMenu) + { contextMenu.Items.Add( MenuItemHelper.CreateMenuItem( - IconSourcePath.StreamMember, - ContextMenuName.ExpandMembers, + IconSourcePath.DerivedStream, + ContextMenuName.AddMemberDerivedStreams, new VisualizationCommand(() => { - this.AddDerivedMemberStreamChildren(); + this.AddMemberDerivedStreams(out var membersNotAdded); this.ExpandAll(); + + if (membersNotAdded.Any()) + { + Application.Current.Dispatcher.BeginInvoke((Action)(() => + { + new MessageBoxWindow( + Application.Current.MainWindow, + "Warning", + $"The following member(s) were not added because children with the same names already exist: {membersNotAdded.EnumerableToString(", ")}.", + "Close", + null).ShowDialog(); + })); + } }), - isEnabled: this.CanExpandDerivedMemberStreams())); + isEnabled: this.CanAddMemberDerivedStreams())); } /// - /// Gets a value indicating whether this stream tree node can expand derived members. + /// Populates a specified context menu with expand dictionary command. /// - /// True if the stream tree node can expand derived members. - protected virtual bool CanExpandDerivedMemberStreams() + /// The context menu to populate. + private void PopulateContextMenuWithAddDictionaryKeyDerivedStreams(ContextMenu contextMenu) { - // If we have already expanded this node with derived member streams - if (this.InternalChildren.Any(c => c is DerivedMemberStreamTreeNode)) + // Only add dictionary expansion menu item if the stream is a dictionary + if (this.CanAddDictionaryKeyDerivedStreams()) { - // Then no longer expand - return false; + var typeParams = this.DataType.GetGenericArguments(); + var addDictionaryKeyDerivedStreams = typeof(StreamTreeNode).GetMethod(nameof(this.AddDictionaryKeyDerivedStreams), BindingFlags.NonPublic | BindingFlags.Instance).MakeGenericMethod(typeParams); + contextMenu.Items.Add( + MenuItemHelper.CreateMenuItem( + IconSourcePath.DerivedStream, + ContextMenuName.AddDictionaryKeyDerivedStreams, + new VisualizationCommand(() => + { + var parameters = new object[] { null }; + addDictionaryKeyDerivedStreams.Invoke(this, parameters); + var keysNotAdded = (List)parameters[0]; + this.ExpandAll(); + + if (keysNotAdded.Any()) + { + Application.Current.Dispatcher.BeginInvoke((Action)(() => + { + new MessageBoxWindow( + Application.Current.MainWindow, + "Warning", + $"The following keys(s) were not added because children with the same names already exist: {keysNotAdded.EnumerableToString(", ")}.", + "Close", + null).ShowDialog(); + })); + } + }))); } + } + + /// + /// Populates a specified context menu with Add Script Derived Stream command. + /// + /// The context menu to populate. + private void PopulateContextMenuWithAddScriptDerivedStream(ContextMenu contextMenu) + { + contextMenu.Items.Add( + MenuItemHelper.CreateMenuItem( + IconSourcePath.DerivedStream, + ContextMenuName.AddScriptDerivedStream, + new VisualizationCommand(() => + { + if (this.AddScriptDerivedStream(out string errorString)) + { + this.ExpandAll(); + } - var nodeType = TypeResolutionHelper.GetVerifiedType(this.DataTypeName); + if (!string.IsNullOrEmpty(errorString)) + { + Application.Current.Dispatcher.BeginInvoke((Action)(() => + { + new MessageBoxWindow( + Application.Current.MainWindow, + "Error", + errorString, + "Close", + null).ShowDialog(); + })); + } + }))); + } - if (nodeType != null) + /// + /// Populates a specified context menu with edit script command. + /// + /// The context menu to populate. + private void PopulateContextMenuWithModifyScript(ContextMenu contextMenu) + { + if (this.IsScriptDerivedStream) { - return nodeType.GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(property => !property.GetMethod.GetParameters().Any()).Any() || - nodeType.GetFields(BindingFlags.Public | BindingFlags.Instance).Any(); - } + if (contextMenu.Items.Count > 0) + { + contextMenu.Items.Add(new Separator()); + } - return false; + contextMenu.Items.Add( + MenuItemHelper.CreateMenuItem( + IconSourcePath.DerivedStream, + ContextMenuName.ModifyScript, + new VisualizationCommand(() => + { + this.ModifyDerivedStreamScript(out string errorString); + + if (!string.IsNullOrEmpty(errorString)) + { + Application.Current.Dispatcher.BeginInvoke((Action)(() => + { + new MessageBoxWindow( + Application.Current.MainWindow, + "Error", + errorString, + "Close", + null).ShowDialog(); + })); + } + }))); + } } /// /// Populates a specified context menu with zoom to stream command. /// /// The context menu to populate. - protected void PopulateContextMenuWithZoomToStream(ContextMenu contextMenu) + private void PopulateContextMenuWithZoomToStream(ContextMenu contextMenu) { if (contextMenu.Items.Count > 0) { @@ -772,85 +1619,693 @@ protected void PopulateContextMenuWithZoomToStream(ContextMenu contextMenu) commandParameter: this)); } + /// + /// Populates a context menu with the expand and collapse all items. + /// + /// The context menu to populate. + private void PopulateContextMenuWithExpandAndCollapseAll(ContextMenu contextMenu) + { + if (!this.internalChildren.Any()) + { + return; + } + + if (contextMenu.Items.Count > 0) + { + contextMenu.Items.Add(new Separator()); + } + + contextMenu.Items.Add( + MenuItemHelper.CreateMenuItem( + IconSourcePath.ExpandAllNodes, + ContextMenuName.ExpandAllNodes, + new VisualizationCommand(() => this.ExpandAll()), + isEnabled: this.internalChildren.Any())); + + contextMenu.Items.Add( + MenuItemHelper.CreateMenuItem( + IconSourcePath.CollapseAllNodes, + ContextMenuName.CollapseAllNodes, + new VisualizationCommand(() => this.CollapseAll()), + isEnabled: this.internalChildren.Any())); + } + + /// + /// Populates a context menu with the commands for showing stream info. + /// + /// The context menu to populate. + private void PopulateContextMenuWithShowStreamInfo(ContextMenu contextMenu) + { + if (contextMenu.Items.Count > 0) + { + contextMenu.Items.Add(new Separator()); + } + + // Add run batch processing task menu + var showStreamInfoMenuItem = MenuItemHelper.CreateMenuItem(string.Empty, "Show Streams Info", null); + foreach (var auxiliaryStreamInfo in Enum.GetValues(typeof(AuxiliaryStreamInfo))) + { + var auxiliaryStreamInfoValue = (AuxiliaryStreamInfo)auxiliaryStreamInfo; + var auxiliaryStreamInfoName = auxiliaryStreamInfoValue switch + { + AuxiliaryStreamInfo.None => "None", + AuxiliaryStreamInfo.MessageCount => "Message Count", + AuxiliaryStreamInfo.AverageMessageSize => "Average Message Size", + AuxiliaryStreamInfo.AverageMessageLatencyMs => "Average Message Latency", + AuxiliaryStreamInfo.Size => "Size", + AuxiliaryStreamInfo.DataThroughputPerHour => "Throughput (bytes per hour)", + AuxiliaryStreamInfo.DataThroughputPerMinute => "Throughput (bytes per minute)", + AuxiliaryStreamInfo.DataThroughputPerSecond => "Throughput (bytes per second)", + AuxiliaryStreamInfo.MessageCountThroughputPerHour => "Throughput (messages per hour)", + AuxiliaryStreamInfo.MessageCountThroughputPerMinute => "Throughput (messages per minute)", + AuxiliaryStreamInfo.MessageCountThroughputPerSecond => "Throughput (messages per second)", + _ => throw new NotImplementedException(), + }; + + showStreamInfoMenuItem.Items.Add( + MenuItemHelper.CreateMenuItem( + this.DatasetViewModel.ShowAuxiliaryStreamInfo == auxiliaryStreamInfoValue ? IconSourcePath.Checkmark : null, + auxiliaryStreamInfoName, + new VisualizationCommand(s => this.DatasetViewModel.ShowAuxiliaryStreamInfo = s), + commandParameter: auxiliaryStreamInfoValue)); + } + + contextMenu.Items.Add(showStreamInfoMenuItem); + } + /// /// Creates a menu item for visualizing the stream. /// /// The visualizer metadata. /// Indicates whether to show the icon. /// The menu item. - protected MenuItem CreateVisualizeStreamMenuItem(VisualizerMetadata metadata, bool showIcon = true) - { - return MenuItemHelper.CreateMenuItem( + private MenuItem CreateVisualizeStreamMenuItem(VisualizerMetadata metadata, bool showIcon = true) + => MenuItemHelper.CreateMenuItem( showIcon ? metadata.IconSourcePath : string.Empty, metadata.CommandText, - new VisualizationCommand(m => VisualizationContext.Instance.VisualizeStream(this, m, VisualizationContext.Instance.VisualizationContainer.CurrentPanel)), + new VisualizationCommand( + m => VisualizationContext.Instance.VisualizeStream( + this, m, VisualizationContext.Instance.VisualizationContainer.CurrentPanel)), tag: metadata, commandParameter: metadata); - } - /// - protected override void OnDatasetViewModelPropertyChanged(object sender, PropertyChangedEventArgs e) + /// + /// Adds a new stream tree node as child of this node. + /// + /// An enumeration containing the path to the child stream tree node to be added. + /// The source stream metadata. + /// The derived stream adapter type. + /// The derived stream adapter arguments. + /// Whether to add the new node in sorted order. + /// A reference to the child stream tree node if successfully created, or null otherwise. + private StreamTreeNode AddChild( + IEnumerable childPathItems, + IStreamMetadata sourceStreamMetadata, + Type derivedStreamAdapterType, + object[] derivedStreamAdapterArguments, + bool sorted = false) { - base.OnDatasetViewModelPropertyChanged(sender, e); - if (e.PropertyName == nameof(this.PartitionViewModel.SessionViewModel.DatasetViewModel.CurrentSessionViewModel)) + var firstPathItem = childPathItems.First(); + + // Add a container if one does not already exists for the first path element + var firstPathItemNode = this.internalChildren.FirstOrDefault(p => p.Name == firstPathItem); + if (firstPathItemNode == null) { - this.RaisePropertyChanged(nameof(this.IsInCurrentSession)); + firstPathItemNode = new StreamTreeNode(this.PartitionViewModel, this.FullName == null ? firstPathItem : $"{this.FullName}.{firstPathItem}", null, null, null); + if (sorted) + { + this.internalChildren.InsertSorted(firstPathItemNode, (node1, node2) => string.Compare(node1.Name, node2.Name)); + } + else + { + this.internalChildren.Add(firstPathItemNode); + } + } + + // If we are at the last segment of the path name then we are at the leaf node + if (childPathItems.Count() == 1) + { + // If the first path item node is a container + if (firstPathItemNode.IsContainer) + { + // Then convert the container to a stream + + // Find the index and remove the container node + var index = this.internalChildren.IndexOf(firstPathItemNode); + this.internalChildren.Remove(firstPathItemNode); + + var streamTreeNode = new StreamTreeNode( + this.PartitionViewModel, + firstPathItemNode.FullName, + sourceStreamMetadata, + derivedStreamAdapterType, + derivedStreamAdapterArguments); + + // Add any children the previous container might have had to the stream tree node + foreach (var child in firstPathItemNode.Children) + { + streamTreeNode.internalChildren.Add(child); + } + + // Insert the new node at the same position as the container + this.internalChildren.Insert(index, streamTreeNode); + + // Determine whether this node has source stream descendants as a result of this addition + if (firstPathItemNode.HasSourceStreamDescendants || streamTreeNode.IsSourceStream) + { + this.HasSourceStreamDescendants = true; + } + + return streamTreeNode; + } + else + { + // O/w a stream already exists at the desired position, so we cannot effect the addition + return null; + } } + + // We are not at the last segment so recurse in + var childStreamTreeNode = firstPathItemNode.AddChild(childPathItems.Skip(1), sourceStreamMetadata, derivedStreamAdapterType, derivedStreamAdapterArguments, sorted); + + // If we have successfully added a child which corresponds to a real stream + if (childStreamTreeNode != null && childStreamTreeNode.IsSourceStream) + { + // Then mark that we have stream descendants. + this.HasSourceStreamDescendants = true; + } + + return childStreamTreeNode; } - /// - protected override void UpdateAuxiliaryInfo() + /// + /// Selects a child tree node and expands all nodes on the path to it. + /// + /// An enumeration containing the path to the child stream tree node to be added. + /// True if the child was found, otherwise false. + private bool SelectChild(IEnumerable childPathItems) { - var indexedMarker = this.IsIndexedPsiStream ? "*" : string.Empty; - var streamContainerPreamble = string.Empty; - var hasNonDerivedChildren = this.InternalChildren.Where(c => c is not DerivedStreamTreeNode).Any(); - switch (this.PartitionViewModel.SessionViewModel.DatasetViewModel.ShowAuxiliaryStreamInfo) + var firstPathItem = childPathItems.First(); + var firstPathItemNode = this.internalChildren.FirstOrDefault(p => p.Name == firstPathItem); + if (firstPathItemNode == default) { - case AuxiliaryStreamInfo.None: - this.AuxiliaryInfo = string.Empty; - break; - case AuxiliaryStreamInfo.Size: - streamContainerPreamble = hasNonDerivedChildren ? $"[{SizeFormatHelper.FormatSize(this.SubsumedSize)}] " : string.Empty; - this.AuxiliaryInfo = streamContainerPreamble + indexedMarker + SizeFormatHelper.FormatSize(this.SourceStreamSize); - break; - case AuxiliaryStreamInfo.DataThroughputPerHour: - streamContainerPreamble = hasNonDerivedChildren ? $"[{SizeFormatHelper.FormatThroughput(this.SubsumedSize / (this.SubsumedClosedTime - this.SubsumedOpenedTime).TotalHours, "hour")}] " : string.Empty; - this.AuxiliaryInfo = streamContainerPreamble + indexedMarker + SizeFormatHelper.FormatThroughput(this.SourceStreamSize / (this.SourceStreamClosedTime - this.SourceStreamOpenedTime).TotalHours, "hour"); - break; - case AuxiliaryStreamInfo.DataThroughputPerMinute: - streamContainerPreamble = hasNonDerivedChildren ? $"[{SizeFormatHelper.FormatThroughput(this.SubsumedSize / (this.SubsumedClosedTime - this.SubsumedOpenedTime).TotalMinutes, "min")}] " : string.Empty; - this.AuxiliaryInfo = streamContainerPreamble + indexedMarker + SizeFormatHelper.FormatThroughput(this.SourceStreamSize / (this.SourceStreamClosedTime - this.SourceStreamOpenedTime).TotalMinutes, "min"); - break; - case AuxiliaryStreamInfo.DataThroughputPerSecond: - streamContainerPreamble = hasNonDerivedChildren ? $"[{SizeFormatHelper.FormatThroughput(this.SubsumedSize / (this.SubsumedClosedTime - this.SubsumedOpenedTime).TotalSeconds, "sec")}] " : string.Empty; - this.AuxiliaryInfo = streamContainerPreamble + indexedMarker + SizeFormatHelper.FormatThroughput(this.SourceStreamSize / (this.SourceStreamClosedTime - this.SourceStreamOpenedTime).TotalSeconds, "sec"); - break; - case AuxiliaryStreamInfo.MessageCountThroughputPerHour: - streamContainerPreamble = hasNonDerivedChildren ? $"[{this.SubsumedMessageCount / (this.SubsumedClosedTime - this.SubsumedOpenedTime).TotalHours:0.01}] " : string.Empty; - this.AuxiliaryInfo = streamContainerPreamble + indexedMarker + $"{this.SourceStreamMessageCount / (this.SourceStreamClosedTime - this.SourceStreamOpenedTime).TotalHours:0.01}"; - break; - case AuxiliaryStreamInfo.MessageCountThroughputPerMinute: - streamContainerPreamble = hasNonDerivedChildren ? $"[{this.SubsumedMessageCount / (this.SubsumedClosedTime - this.SubsumedOpenedTime).TotalMinutes:0.01}] " : string.Empty; - this.AuxiliaryInfo = streamContainerPreamble + indexedMarker + $"{this.SourceStreamMessageCount / (this.SourceStreamClosedTime - this.SourceStreamOpenedTime).TotalMinutes:0.01}"; - break; - case AuxiliaryStreamInfo.MessageCountThroughputPerSecond: - streamContainerPreamble = hasNonDerivedChildren ? $"[{this.SubsumedMessageCount / (this.SubsumedClosedTime - this.SubsumedOpenedTime).TotalSeconds:0.01}] " : string.Empty; - this.AuxiliaryInfo = streamContainerPreamble + indexedMarker + $"{this.SourceStreamMessageCount / (this.SourceStreamClosedTime - this.SourceStreamOpenedTime).TotalSeconds:0.01}"; - break; - case AuxiliaryStreamInfo.MessageCount: - streamContainerPreamble = hasNonDerivedChildren ? this.SubsumedMessageCount == 0 ? "[0] " : $"[{this.SubsumedMessageCount:0,0}] " : string.Empty; - this.AuxiliaryInfo = streamContainerPreamble + (this.SourceStreamMessageCount == 0 ? "0" : $"{this.SourceStreamMessageCount:0,0}"); - break; - case AuxiliaryStreamInfo.AverageMessageLatencyMs: - streamContainerPreamble = hasNonDerivedChildren ? $"[{SizeFormatHelper.FormatLatencyMs(this.SubsumedAverageMessageLatencyMs)}] " : string.Empty; - this.AuxiliaryInfo = streamContainerPreamble + SizeFormatHelper.FormatLatencyMs(this.SubsumedAverageMessageLatencyMs); - break; - case AuxiliaryStreamInfo.AverageMessageSize: - streamContainerPreamble = hasNonDerivedChildren ? $"[{SizeFormatHelper.FormatSize(this.SubsumedAverageMessageSize)}] " : string.Empty; - this.AuxiliaryInfo = streamContainerPreamble + indexedMarker + SizeFormatHelper.FormatSize(this.SourceStreamAverageMessageSize); - break; - default: + return false; + } + + if (childPathItems.Count() == 1) + { + firstPathItemNode.IsTreeNodeSelected = true; + this.IsTreeNodeExpanded = true; + return true; + } + else + { + bool result = firstPathItemNode.SelectChild(childPathItems.Skip(1)); + if (result) + { + this.IsTreeNodeExpanded = true; + } + + return result; + } + } + + /// + /// Finds a child tree node by name. + /// + /// An enumeration containing the path to the child stream tree node to be added. + /// The stream tree node corresponding to the child if found, or null otherwise. + private StreamTreeNode FindChild(IEnumerable childPathItems) + { + var firstPathItem = childPathItems.First(); + var firstPathItemNode = this.internalChildren.FirstOrDefault(p => p.Name == firstPathItem); + if (firstPathItemNode == default) + { + return null; + } + + if (childPathItems.Count() == 1) + { + return firstPathItemNode; + } + else + { + return firstPathItemNode.FindChild(childPathItems.Skip(1)); + } + } + + /// + /// Customizes a list of visualizers by inserting custom adapters where necessary. + /// + /// The list of visualizers. + private void InsertCustomAdapters(List metadatas) + { + // For each of the non-universal visualization objects, add a data adapter from the stream data type to the subfield data type + for (int index = 0; index < metadatas.Count; index++) + { + // For message visualization object insert a custom object adapter so values can be displayed for known types. + if (metadatas[index].VisualizationObjectType == typeof(MessageVisualizationObject)) + { + var objectAdapterType = typeof(ObjectAdapter<>).MakeGenericType(this.DataType); + metadatas[index] = metadatas[index].GetCloneWithNewStreamAdapterType(objectAdapterType); + } + else if (metadatas[index].VisualizationObjectType == typeof(LatencyVisualizationObject)) + { + // o/w for latency visualization object insert a custom object adapter so values can be displayed for known types. + var objectToLatencyAdapterType = typeof(ObjectToLatencyAdapter<>).MakeGenericType(this.DataType); + metadatas[index] = metadatas[index].GetCloneWithNewStreamAdapterType(objectToLatencyAdapterType); + } + else if (metadatas[index].StreamAdapterType == null && metadatas[index].DataType.IsInterface) + { + // o/w for interface types inject an interface adapter + var interfaceAdapterType = typeof(InterfaceAdapter<,>).MakeGenericType(this.DataType, metadatas[index].DataType); + metadatas[index] = metadatas[index].GetCloneWithNewStreamAdapterType(interfaceAdapterType); + } + } + } + + /// + /// Attempts to add all member derived stream children. + /// + /// An output parameter containing all the members that were not added (b/c of already existing children). + private void AddMemberDerivedStreams(out List membersNotAdded) + { + // Maintain the set of properties that could not be added. + membersNotAdded = new List(); + + // Determine if the current node is a reference or nullable type + var dataTypeIsReferenceOrNullable = this.IsReferenceOrNullableType(this.DataType); + + var dataType = this.DataType; + string memberPathPrefix = string.Empty; + + // Member expansion of Nullable types needs to be handled slightly differently by adding a + // [HasValue] node representing the Nullable.HasValue property, followed by the members of + // the underlying Value if it can be further expanded. We wrap the HasValue node in []s to + // prevent any possible clash if the underlying type also contains a member named HasValue. + if (this.IsNullableDataType) + { + // HasValue always has a value, so we can simply expand it as a bool (generateNullableMemberType = false) + this.AddMemberDerivedStream(this, "[HasValue]", "HasValue", typeof(bool), generateNullableMemberType: false); + + // This will cause the members of the underlying Nullable Value to be expanded below + memberPathPrefix = "Value."; + dataType = Nullable.GetUnderlyingType(this.DataType); + } + + // Create a child node for each public instance property that takes no parameters. + foreach (var propertyInfo in dataType.GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(property => !property.GetMethod.GetParameters().Any())) + { + var child = this.AddMemberDerivedStream(this, propertyInfo.Name, memberPathPrefix + propertyInfo.Name, propertyInfo.PropertyType, dataTypeIsReferenceOrNullable && !this.IsReferenceOrNullableType(propertyInfo.PropertyType)); + if (child == null) + { + membersNotAdded.Add(propertyInfo.Name); + } + } + + // Create a child node for each public instance field + foreach (var fieldInfo in dataType.GetFields(BindingFlags.Public | BindingFlags.Instance)) + { + var child = this.AddMemberDerivedStream(this, fieldInfo.Name, memberPathPrefix + fieldInfo.Name, fieldInfo.FieldType, dataTypeIsReferenceOrNullable && !this.IsReferenceOrNullableType(fieldInfo.FieldType)); + if (child == null) + { + membersNotAdded.Add(fieldInfo.Name); + } + } + } + + /// + /// Gets a value indicating whether this stream tree node can add derived member streams. + /// + /// True if the stream tree node can add derived member streams. + private bool CanAddMemberDerivedStreams() + => this.DataType.GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(property => !property.GetMethod.GetParameters().Any()).Any() || + this.DataType.GetFields(BindingFlags.Public | BindingFlags.Instance).Any(); + + /// + /// Gets a value indicating whether a specified type is a reference or nullable type. + /// + /// The type to check. + /// True if the specified type is a reference or nullable type. + private bool IsReferenceOrNullableType(Type type) + => !type.IsValueType || Nullable.GetUnderlyingType(type) != null; + + /// + /// Adds a derived member stream child. + /// + /// The parent stream tree node. + /// The name of the child node to add. + /// The path to the member. + /// The member type. + /// Indicates whether to generate a nullable member type expansion. + private StreamTreeNode AddMemberDerivedStream(StreamTreeNode parent, string nodeName, string memberPath, Type memberType, bool generateNullableMemberType) + { + // Compute the memberType + if (generateNullableMemberType) + { + memberType = typeof(Nullable<>).MakeGenericType(memberType); + } + + // Compute the member adapter type name and its parameters + var derivedStreamMemberAdapterType = typeof(StreamMemberAdapter<,>).MakeGenericType(parent.DataType, memberType); + var derivedStreamMemberAdapterArguments = new object[] { memberPath }; + + // Now, if the parent is also a derived stream tree node + if (parent.IsDerivedStream) + { + // Then update the stream adapter by chaining + derivedStreamMemberAdapterType = typeof(ChainedStreamAdapter<,,,,>) + .MakeGenericType( + VisualizationContext.Instance.GetDataType(parent.SourceStreamMetadata.TypeName), + parent.DataType, + memberType, + parent.DerivedStreamAdapterType, + derivedStreamMemberAdapterType); + + // And the parameters + derivedStreamMemberAdapterArguments = new object[] { parent.DerivedStreamAdapterArguments, derivedStreamMemberAdapterArguments }; + } + + // Construct the member stream node to add + return this.AddChild(nodeName, this.SourceStreamMetadata, derivedStreamMemberAdapterType, derivedStreamMemberAdapterArguments, true); + } + + /// + /// Attempts to add all dictionary key stream children. + /// + /// The dictionary key type. + /// The dictionary value type. + /// An output parameter containing all the keys that were not added (b/c of already existing keys). + private void AddDictionaryKeyDerivedStreams(out List keysNotAdded) + { + var keysToAdd = new Dictionary(); + keysNotAdded = new List(); + + // Create the progress dialog to displayed while we traverse the entire stream for keys + var progressWindow = new ProgressWindow(Application.Current.MainWindow, $"Extracting dictionary keys", showCancelButton: true); + bool? dialogResult; + + void GetAllDictionaryKeys(Dictionary dictionary, Envelope envelope) + { + string errorString = default; + + foreach (var kvp in dictionary) + { + var keyString = kvp.Key.ToString(); + + // If we have already seen this key string, it had better represent the same key (in the Equals() sense) + if (keysToAdd.TryGetValue(keyString, out var existingKey)) + { + if (!kvp.Key.Equals(existingKey)) + { + errorString = $"Could not expand the dictionary keys as the dictionary contains multiple keys which map to the same key string: {keyString}."; + } + } + else if (keyString.Contains('.')) + { + errorString = $"Could not expand the dictionary keys as one of the key strings contains an illegal character [.]: {keyString}."; + } + else + { + // OK to add this new key + keysToAdd.Add(keyString, kvp.Key); + } + + // If there was a problem with any key, abort the pipeline and display an error + if (!string.IsNullOrEmpty(errorString)) + { + // Abort without adding any keys + keysToAdd.Clear(); + + // Setting the DialogResult to false will close the modal dialog window and dispose the pipeline + Application.Current.Dispatcher.Invoke(() => progressWindow.DialogResult = false); + + // Display an error dialog + Application.Current.Dispatcher.BeginInvoke((Action)(() => + { + new MessageBoxWindow( + Application.Current.MainWindow, + "Error", + errorString, + "Close", + null).ShowDialog(); + })); + + return; + } + } + } + + // Create and run a pipeline to read all the keys from the store. This will also perform checks to + // ensure that all keys map to a unique key string and that no key string contains a '.' character. + using (var pipeline = Pipeline.Create(deliveryPolicy: DeliveryPolicy.SynchronousOrThrottle)) + { + var reader = StreamReader.Create(this.PartitionViewModel.StoreName, this.PartitionViewModel.StorePath, this.PartitionViewModel.Partition.StreamReaderTypeName); + var store = new Importer(pipeline, reader, usePerStreamReaders: true); + + if (!this.IsDerivedStream) + { + // Apply the GetAllDictionaryKeys function directly to the source stream + var stream = store.OpenStream>(this.SourceStreamMetadata.Name); + stream.Do(GetAllDictionaryKeys); + } + else + { + // If this is a derived stream, we need to adapt the GetAllDictionaryKeys function using the derived stream adapter + dynamic derivedStreamAdapter = Activator.CreateInstance(this.DerivedStreamAdapterType, this.DerivedStreamAdapterArguments); + dynamic adaptedGetAllDictionaryKeys = derivedStreamAdapter.AdaptReceiver(new Action, Envelope>(GetAllDictionaryKeys)); + + // Apply the adapted GetAllDictionaryKeys function to the source stream + var stream = store.OpenStream(this.SourceStreamMetadata.Name, derivedStreamAdapter.SourceAllocator, derivedStreamAdapter.SourceDeallocator); + Psi.Operators.Do(stream, adaptedGetAllDictionaryKeys); + } + + IProgress progress = new Progress(p => + { + progressWindow.Progress = p; + + if (p == 1.0 && !progressWindow.DialogResult.HasValue) + { + // close the progress window when the pipeline reports completion + progressWindow.DialogResult = true; + } + }); + + pipeline.RunAsync(ReplayDescriptor.ReplayAll, progress); + dialogResult = progressWindow.ShowDialog(); + } + + // This means the key extraction pipeline ran to completion successfully and we can add the keys + if (dialogResult == true) + { + // Create a child node for each key + foreach (var keyString in keysToAdd.Keys) + { + var child = this.AddDictionaryKeyDerivedStream(this, keyString, !this.IsReferenceOrNullableType(typeof(TValue))); + if (child == null) + { + keysNotAdded.InsertSorted(keyString); + } + } + } + } + + /// + /// Gets a value indicating whether this stream tree node can add derived dictionary key streams. + /// + /// True if the stream tree node can add derived dictionary key streams. + private bool CanAddDictionaryKeyDerivedStreams() + => this.DataType.IsGenericType && this.DataType.GetGenericTypeDefinition() == typeof(Dictionary<,>); + + /// + /// Adds a dictionary key stream child. + /// + /// The parent stream tree node. + /// The dictionary key string. + /// Indicates whether to generate a nullable dictionary value type expansion. + private StreamTreeNode AddDictionaryKeyDerivedStream(StreamTreeNode parent, string keyString, bool generateNullableValueType) + { + var destinationType = typeof(TValue); + + // Compute the memberType + if (generateNullableValueType) + { + destinationType = typeof(Nullable<>).MakeGenericType(destinationType); + } + + // Compute the dictionary key-to-value adapter type name and its parameters + var derivedStreamKeyToValueAdapterType = typeof(DictionaryKeyToValueAdapter<,,>).MakeGenericType(typeof(TKey), typeof(TValue), destinationType); + var derivedStreamKeyToValueAdapterArguments = new object[] { keyString }; + + // Now, if the parent is also a derived stream tree node + if (parent.IsDerivedStream) + { + // Then update the stream adapter by chaining + derivedStreamKeyToValueAdapterType = typeof(ChainedStreamAdapter<,,,,>) + .MakeGenericType( + VisualizationContext.Instance.GetDataType(parent.SourceStreamMetadata.TypeName), + parent.DataType, + destinationType, + parent.DerivedStreamAdapterType, + derivedStreamKeyToValueAdapterType); + + // And the parameters + derivedStreamKeyToValueAdapterArguments = new object[] { parent.DerivedStreamAdapterArguments, derivedStreamKeyToValueAdapterArguments }; + } + + // Construct the member stream node to add + return this.AddChild($"[{keyString}]", this.SourceStreamMetadata, derivedStreamKeyToValueAdapterType, derivedStreamKeyToValueAdapterArguments, true); + } + + /// + /// Attempts to add a script derived stream. + /// + /// An output parameter representing an error string. + /// Whether or not the derived stream tree node was added. + private bool AddScriptDerivedStream(out string errorString) + { + errorString = null; + + var scriptWindow = new ScriptWindow(Application.Current.MainWindow, this); + + // Show the script editor dialog + if (scriptWindow.ShowDialog() == true) + { + var script = scriptWindow.ScriptText; + var usings = scriptWindow.Usings; + var scriptName = scriptWindow.ScriptDerivedStreamName; + var returnType = scriptWindow.ReturnType; + + // Compute the scripting adapter type name and its parameters + var derivedScriptedStreamAdapterType = typeof(ScriptAdapter<,>).MakeGenericType(this.DataType, returnType); + var derivedScriptedStreamAdapterArguments = new object[] { script, usings }; + + // Now, if this is also a derived stream tree node + if (this.IsDerivedStream) + { + // Then update the stream adapter by chaining + derivedScriptedStreamAdapterType = typeof(ChainedStreamAdapter<,,,,>) + .MakeGenericType( + VisualizationContext.Instance.GetDataType(this.SourceStreamMetadata.TypeName), + this.DataType, + returnType, + this.DerivedStreamAdapterType, + derivedScriptedStreamAdapterType); + + // And the parameters + derivedScriptedStreamAdapterArguments = new object[] { this.DerivedStreamAdapterArguments, derivedScriptedStreamAdapterArguments }; + } + + // Construct the script stream node to add + var child = this.AddChild($"{scriptName}", this.SourceStreamMetadata, derivedScriptedStreamAdapterType, derivedScriptedStreamAdapterArguments); + if (child == null) + { + errorString = $"This node already contains a script derived stream named {scriptName}."; + } + + return child != null; + } + + return false; + } + + /// + /// Edits the script of an existing script-derived stream. + /// + /// An output parameter representing an error string. + private void ModifyDerivedStreamScript(out string errorString) + { + if (this.TryGetScriptParameters(out string script, out var usings)) + { + var parent = this.PartitionViewModel.FindStreamTreeNode(this.FullName.Substring(0, this.FullName.LastIndexOf('.'))); + var scriptWindow = new ScriptWindow(Application.Current.MainWindow, parent, false) + { + ScriptText = script, + ScriptDerivedStreamName = this.Name, + ReturnType = this.DataType, + Usings = new ObservableCollection(usings), + }; + + // Allow editing of existing script (but not the script name or return type as those are already baked into this node) + if (scriptWindow.ShowDialog() == true) + { + this.SetScriptParameters(scriptWindow.ScriptText, scriptWindow.Usings); + } + + errorString = null; + } + else + { + errorString = "Failed to retrieve the script parameters from the adapter."; + } + } + + /// + /// Attempts to get the script parameters from the last adapter in the chain. + /// + /// A string containing the script code. + /// An enumeration of usings required by the script. + /// A flag indicating whether the script parameters were found. + private bool TryGetScriptParameters(out string scriptText, out IEnumerable usings) + { + Type currentAdapterType = this.DerivedStreamAdapterType; + object[] currentAdapterArguments = this.DerivedStreamAdapterArguments; + + // search for the final ScriptingAdapter + while (currentAdapterType.IsGenericType) + { + if (currentAdapterType.GetGenericTypeDefinition() == typeof(ScriptAdapter<,>)) + { + scriptText = currentAdapterArguments[0] as string; + usings = currentAdapterArguments[1] as IEnumerable; + return true; + } + else if (currentAdapterType.GetGenericTypeDefinition() == typeof(ChainedStreamAdapter<,,,,>)) + { + // continue searching with the second adapter in the chain + currentAdapterType = currentAdapterType.GetGenericArguments()[4]; + currentAdapterArguments = currentAdapterArguments[1] as object[]; + } + else + { break; + } + } + + // Not a derived script stream, or bad arguments. + scriptText = null; + usings = null; + + return false; + } + + /// + /// Sets the script parameters. + /// + /// A string containing the script code. + /// An enumeration of usings required by the script. + private void SetScriptParameters(string scriptText, IEnumerable usings) + { + Type currentAdapterType = this.DerivedStreamAdapterType; + object[] currentAdapterArguments = this.DerivedStreamAdapterArguments; + + while (currentAdapterType.IsGenericType) + { + if (currentAdapterType.GetGenericTypeDefinition() == typeof(ScriptAdapter<,>)) + { + currentAdapterArguments[0] = scriptText; + currentAdapterArguments[1] = usings; + return; + } + else if (currentAdapterType.GetGenericTypeDefinition() == typeof(ChainedStreamAdapter<,,,,>)) + { + currentAdapterType = currentAdapterType.GetGenericArguments()[4]; + currentAdapterArguments = currentAdapterArguments[1] as object[]; + } + else + { + throw new InvalidOperationException($"No adapter of type {typeof(ScriptAdapter<,>).Name} found."); + } } } } diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/ContextMenuItemsSourceType.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/ContextMenuItemsSourceType.cs deleted file mode 100644 index 0697c7a06..000000000 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/ContextMenuItemsSourceType.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. - -namespace Microsoft.Psi.Visualization.Views -{ - /// - /// The type of a context menu source. - /// - public enum ContextMenuItemsSourceType - { - /// - /// The context menu source is a visualization object. - /// - VisualizationObject = 1, - - /// - /// The context menu source is a visualization panel. - /// - VisualizationPanel = 2, - - /// - /// The context menu source is a visualization panel matrix container. - /// - VisualizationPanelMatrixContainer = 3, - - /// - /// The context menu source is a visualization container. - /// - VisualizationContainer = 4, - } -} diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/IContextMenuItemsSource.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/IContextMenuItemsSource.cs deleted file mode 100644 index 7bbd529e6..000000000 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/IContextMenuItemsSource.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. - -namespace Microsoft.Psi.Visualization.Views -{ - using System.Collections.Generic; - using System.Windows.Controls; - - /// - /// Represents an object that is capable of supplying context menu items. - /// - public interface IContextMenuItemsSource - { - /// - /// Gets the context menu items source type. - /// - ContextMenuItemsSourceType ContextMenuItemsSourceType { get; } - - /// - /// Gets the name of the context menu items source. - /// - string ContextMenuObjectName { get; } - - /// - /// Gets the context menu items the context menu source wishes to display. - /// - /// A collection of menu items to append to. - void AppendContextMenuItems(List menuItems); - } -} diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/InstantVisualizationContainerView.xaml.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/InstantVisualizationContainerView.xaml.cs index 3a266d211..d5a72bc52 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/InstantVisualizationContainerView.xaml.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/InstantVisualizationContainerView.xaml.cs @@ -4,7 +4,6 @@ namespace Microsoft.Psi.Visualization.Views { using System; - using System.Collections.Generic; using System.Collections.Specialized; using System.Windows; using System.Windows.Controls; @@ -18,89 +17,34 @@ namespace Microsoft.Psi.Visualization.Views /// /// Interaction logic for InstantVisualizationContainerView.xaml. /// - public partial class InstantVisualizationContainerView : UserControl, IContextMenuItemsSource + public partial class InstantVisualizationContainerView : UserControl { - private VisualizationPanel mouseOverVisualizationPanel; - /// /// Initializes a new instance of the class. /// public InstantVisualizationContainerView() { this.InitializeComponent(); - this.DataContextChanged += this.InstantVisualizationContainerView_DataContextChanged; - this.SizeChanged += this.InstantVisualizationContainerView_SizeChanged; + this.DataContextChanged += this.OnInstantVisualizationContainerViewDataContextChanged; + this.SizeChanged += this.OnInstantVisualizationContainerViewSizeChanged; } - /// - public ContextMenuItemsSourceType ContextMenuItemsSourceType => ContextMenuItemsSourceType.VisualizationPanelMatrixContainer; - - /// - public string ContextMenuObjectName => string.Empty; - /// /// Gets the visualization panel. /// protected InstantVisualizationContainer VisualizationPanel => (InstantVisualizationContainer)this.DataContext; - /// - public void AppendContextMenuItems(List menuItems) - { - if (this.DataContext is InstantVisualizationContainer instantVisualizationContainer) - { - // Find the child panel that the mouse is over - // Run a hit test at the mouse cursor - this.mouseOverVisualizationPanel = null; - VisualTreeHelper.HitTest( - this, - null, - new HitTestResultCallback(this.ContextMenuHitTestResult), - new PointHitTestParameters(Mouse.GetPosition(this))); - - if (this.mouseOverVisualizationPanel != null) - { - menuItems.Add(MenuItemHelper.CreateMenuItem(IconSourcePath.InstantContainerAddCellLeft, $"Insert Cell to the Left", instantVisualizationContainer.CreateIncreaseCellCountCommand(this.mouseOverVisualizationPanel, true))); - menuItems.Add(MenuItemHelper.CreateMenuItem(IconSourcePath.InstantContainerAddCellRight, $"Insert Cell to the Right", instantVisualizationContainer.CreateIncreaseCellCountCommand(this.mouseOverVisualizationPanel, false))); - menuItems.Add(MenuItemHelper.CreateMenuItem(null, $"Remove Cell", instantVisualizationContainer.CreateRemoveCellCommand(this.mouseOverVisualizationPanel))); - menuItems.Add(MenuItemHelper.CreateMenuItem(IconSourcePath.InstantContainerRemoveCell, $"Remove {instantVisualizationContainer.Name}", instantVisualizationContainer.RemovePanelCommand)); - } - } - } - - private HitTestResultBehavior ContextMenuHitTestResult(HitTestResult result) - { - // Find the visualization panel view that the mouse is over - DependencyObject dependencyObject = result.VisualHit; - while (dependencyObject != null) - { - // If the dependency object is not a hidden panel - if (!(dependencyObject is VisualizationPanelView visualizationPanelView && !(visualizationPanelView.DataContext as VisualizationPanel).IsShown)) - { - if (dependencyObject is IContextMenuItemsSource contextMenuItemsSource && contextMenuItemsSource.ContextMenuItemsSourceType == ContextMenuItemsSourceType.VisualizationPanel) - { - // Get the visualization panel related to the visualization panel view - this.mouseOverVisualizationPanel = (contextMenuItemsSource as VisualizationPanelView).DataContext as VisualizationPanel; - return HitTestResultBehavior.Stop; - } - } - - dependencyObject = VisualTreeHelper.GetParent(dependencyObject); - } - - return HitTestResultBehavior.Continue; - } - - private void InstantVisualizationContainerView_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e) + private void OnInstantVisualizationContainerViewDataContextChanged(object sender, DependencyPropertyChangedEventArgs e) { if (e.OldValue is InstantVisualizationContainer oldContainer) { - oldContainer.Panels.CollectionChanged -= this.Panels_CollectionChanged; + oldContainer.Panels.CollectionChanged -= this.OnPanelsCollectionChanged; oldContainer.ChildVisualizationPanelWidthChanged -= this.ChildVisualizationPanelWidthChanged; } if (e.NewValue is InstantVisualizationContainer newContainer) { - newContainer.Panels.CollectionChanged += this.Panels_CollectionChanged; + newContainer.Panels.CollectionChanged += this.OnPanelsCollectionChanged; newContainer.ChildVisualizationPanelWidthChanged += this.ChildVisualizationPanelWidthChanged; } } @@ -110,7 +54,7 @@ private void ChildVisualizationPanelWidthChanged(object sender, EventArgs e) this.ResizeChildVisualizationPanels(); } - private void Panels_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + private void OnPanelsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { if (e.Action == NotifyCollectionChangedAction.Add || e.Action == NotifyCollectionChangedAction.Remove) { @@ -118,7 +62,7 @@ private void Panels_CollectionChanged(object sender, NotifyCollectionChangedEven } } - private void InstantVisualizationContainerView_SizeChanged(object sender, SizeChangedEventArgs e) + private void OnInstantVisualizationContainerViewSizeChanged(object sender, SizeChangedEventArgs e) { this.ResizeChildVisualizationPanels(); } diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/InstantVisualizationPanelView.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/InstantVisualizationPanelView.cs index 9b9a55d07..f2e89bf7f 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/InstantVisualizationPanelView.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/InstantVisualizationPanelView.cs @@ -3,83 +3,10 @@ namespace Microsoft.Psi.Visualization.Views { - using System.Collections.Generic; - using System.Linq; - using System.Windows; - using System.Windows.Controls; - using GalaSoft.MvvmLight.CommandWpf; - using Microsoft.Psi.Visualization.Helpers; - using Microsoft.Psi.Visualization.VisualizationPanels; - using Microsoft.Psi.Visualization.Windows; - /// /// Represents the base class for all instant visualization panel views. /// public abstract class InstantVisualizationPanelView : VisualizationPanelView { - /// - public override void AppendContextMenuItems(List menuItems) - { - if (this.DataContext is InstantVisualizationPanel visualizationPanel) - { - // Add Set Cursor Epsilon menu with sub-menu items - var setCursorEpsilonMenuItem = MenuItemHelper.CreateMenuItem( - string.Empty, - "Set Default Cursor Epsilon", - null); - - _ = setCursorEpsilonMenuItem.Items.Add( - MenuItemHelper.CreateMenuItem( - null, - "Infinite Past", - new RelayCommand(() => this.UpdateDefaultCursorEpsilon(visualizationPanel, "Infinite Past", int.MaxValue, 0), true))); - setCursorEpsilonMenuItem.Items.Add( - MenuItemHelper.CreateMenuItem( - null, - "Last 5 seconds", - new RelayCommand(() => this.UpdateDefaultCursorEpsilon(visualizationPanel, "Last 5 seconds", 5000, 0), true))); - setCursorEpsilonMenuItem.Items.Add( - MenuItemHelper.CreateMenuItem( - null, - "Last 1 second", - new RelayCommand(() => this.UpdateDefaultCursorEpsilon(visualizationPanel, "Last 1 second", 1000, 0), true))); - setCursorEpsilonMenuItem.Items.Add( - MenuItemHelper.CreateMenuItem( - null, - "Last 50 milliseconds", - new RelayCommand(() => this.UpdateDefaultCursorEpsilon(visualizationPanel, "Last 50 milliseconds", 50, 0), true))); - - menuItems.Add(setCursorEpsilonMenuItem); - } - - base.AppendContextMenuItems(menuItems); - } - - private void UpdateDefaultCursorEpsilon(InstantVisualizationPanel visualizationPanel, string name, int negMs, int posMs) - { - visualizationPanel.DefaultCursorEpsilonNegMs = negMs; - visualizationPanel.DefaultCursorEpsilonPosMs = posMs; - - var anyVisualizersWithDifferentCursorEpsilon = - visualizationPanel.VisualizationObjects.Any(vo => vo.CursorEpsilonNegMs != 50 || vo.CursorEpsilonPosMs != 0); - - if (anyVisualizersWithDifferentCursorEpsilon) - { - var result = new MessageBoxWindow( - Application.Current.MainWindow, - "Update visualizers?", - $"Some of the visualizers in this panel have a cursor epsilon that is different from {name}. Would you also like to change the cursor epsilon for these visualizers to {name}?", - "Yes", - "No").ShowDialog(); - if (result == true) - { - foreach (var vo in visualizationPanel.VisualizationObjects) - { - vo.CursorEpsilonNegMs = negMs; - vo.CursorEpsilonPosMs = posMs; - } - } - } - } } } diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/InstantVisualizationPlaceholderPanelView.xaml.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/InstantVisualizationPlaceholderPanelView.xaml.cs index 333e3181d..ca4f906f3 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/InstantVisualizationPlaceholderPanelView.xaml.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/InstantVisualizationPlaceholderPanelView.xaml.cs @@ -3,8 +3,6 @@ namespace Microsoft.Psi.Visualization.Views { - using System.Collections.Generic; - using System.Windows.Controls; using Microsoft.Psi.Visualization.VisualizationPanels; /// diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/NavigatorView.xaml.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/NavigatorView.xaml.cs index 5d76f27b1..5f83b02b1 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/NavigatorView.xaml.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/NavigatorView.xaml.cs @@ -8,11 +8,7 @@ namespace Microsoft.Psi.Visualization.Views using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Input; - using System.Windows.Media; - using Microsoft.Psi.Visualization.Data; - using Microsoft.Psi.Visualization.Helpers; using Microsoft.Psi.Visualization.Navigation; - using Microsoft.Psi.Visualization.VisualizationObjects; /// /// Interaction logic for NavigatorView.xaml. diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/TimelineVisualizationPanelView.xaml.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/TimelineVisualizationPanelView.xaml.cs index 3dab44270..be1708cc0 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/TimelineVisualizationPanelView.xaml.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/TimelineVisualizationPanelView.xaml.cs @@ -4,9 +4,7 @@ namespace Microsoft.Psi.Visualization.Views { using System; - using System.Collections.Generic; using System.Windows; - using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; @@ -43,31 +41,6 @@ private enum DragOperation /// private TimelineVisualizationPanel VisualizationPanel => this.DataContext as TimelineVisualizationPanel; - /// - public override void AppendContextMenuItems(List menuItems) - { - if (this.DataContext is TimelineVisualizationPanel timelineVisualizationPanel) - { - // The show/hide legend menu - menuItems.Add( - MenuItemHelper.CreateMenuItem( - IconSourcePath.Legend, - timelineVisualizationPanel.ShowLegend ? $"Hide Legend" : $"Show Legend", - timelineVisualizationPanel.ShowHideLegendCommand)); - - menuItems.Add(MenuItemHelper.CreateMenuItem( - null, - "Auto-Fit Axes", - this.VisualizationPanel.SetAutoAxisComputeModeCommand, - null, - this.VisualizationPanel.AxisComputeMode == AxisComputeMode.Manual)); - - menuItems.Add(null); - } - - base.AppendContextMenuItems(menuItems); - } - /// /// Signals to the panel that a drag and drop operation it may have initiated has been completed. /// diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/VisualizationContainerView.xaml.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/VisualizationContainerView.xaml.cs index fd5a33c8f..343d25283 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/VisualizationContainerView.xaml.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/VisualizationContainerView.xaml.cs @@ -3,7 +3,6 @@ namespace Microsoft.Psi.Visualization.Views { - using System; using System.Collections.Generic; using System.Linq; using System.Windows; @@ -17,19 +16,18 @@ namespace Microsoft.Psi.Visualization.Views using Microsoft.Psi.Visualization.ViewModels; using Microsoft.Psi.Visualization.VisualizationObjects; using Microsoft.Psi.Visualization.VisualizationPanels; - using Xceed.Wpf.AvalonDock.Controls; /// /// Interaction logic for VisualizationContainerView.xaml. /// - public partial class VisualizationContainerView : UserControl, IContextMenuItemsSource + public partial class VisualizationContainerView : UserControl { // This adorner renders a shadow of a Visualization Panel while it's being dragged by the mouse private VisualizationContainerDragDropAdorner dragDropAdorner = null; private VisualizationPanelView hitTestResult = null; // The collection of context menu sources for the context menu that's about to be displayed. - private Dictionary contextMenuSources; + private List contextMenuItemsSources; /// /// Initializes a new instance of the class. @@ -40,23 +38,6 @@ public VisualizationContainerView() this.ContextMenu = new ContextMenu(); } - /// - public ContextMenuItemsSourceType ContextMenuItemsSourceType => ContextMenuItemsSourceType.VisualizationContainer; - - /// - public string ContextMenuObjectName => string.Empty; - - /// - public void AppendContextMenuItems(List menuItems) - { - if (this.DataContext is VisualizationContainer visualizationContainer) - { - menuItems.Add(MenuItemHelper.CreateMenuItem(IconSourcePath.ZoomToSelection, "Zoom to Selection", visualizationContainer.ZoomToSelectionCommand)); - menuItems.Add(MenuItemHelper.CreateMenuItem(IconSourcePath.ClearSelection, "Clear Selection", visualizationContainer.ClearSelectionCommand)); - menuItems.Add(MenuItemHelper.CreateMenuItem(IconSourcePath.ZoomToSession, "Zoom to Session Extents", visualizationContainer.ZoomToSessionExtentsCommand)); - } - } - private void Items_DragEnter(object sender, DragEventArgs e) { // Make sure this is an object we care about @@ -271,8 +252,11 @@ private void OnContextMenuOpening(object sender, ContextMenuEventArgs e) return; } + // Clear the existing context menu + this.ContextMenu.Items.Clear(); + // Create a new context menu metadata to be filled by the hit tester - this.contextMenuSources = new Dictionary(); + this.contextMenuItemsSources = new List(); // Run a hit test at the mouse cursor VisualTreeHelper.HitTest( @@ -281,43 +265,42 @@ private void OnContextMenuOpening(object sender, ContextMenuEventArgs e) new HitTestResultCallback(this.ContextMenuHitTestResult), new PointHitTestParameters(Mouse.GetPosition(this))); - // If a visualization panel was found, set it as the current visualization panel. - if (this.contextMenuSources.ContainsKey(ContextMenuItemsSourceType.VisualizationPanel)) + // If a visualization panel is amount the sources, set it as the current visualization panel. + if (this.contextMenuItemsSources.FirstOrDefault(s => s is VisualizationPanel) is VisualizationPanel visualizationPanel) { - VisualizationPanel visualizationPanel = (this.contextMenuSources[ContextMenuItemsSourceType.VisualizationPanel] as VisualizationPanelView).DataContext as VisualizationPanel; + // Compute the visualization panel corresponding for that view visualizationPanel.IsTreeNodeSelected = true; - } - - // Clear the existing context menu - this.ContextMenu.Items.Clear(); - - if (this.contextMenuSources.ContainsKey(ContextMenuItemsSourceType.VisualizationPanel)) - { - // Find all of the visualization object views that are children of the panel view. - VisualizationPanelView panelView = this.contextMenuSources[ContextMenuItemsSourceType.VisualizationPanel] as VisualizationPanelView; - IEnumerable visualizationObjectViews = panelView.FindVisualChildren(); - IEnumerator viewEnumerator = visualizationObjectViews.GetEnumerator(); // If there's only a single visualization object view then insert its context menu items // inline, otherwise generate a separate cascading menu for each visualization object view. - if (visualizationObjectViews.Count() == 1) + bool addVisualizationObjectCommandsInSubmenus = visualizationPanel.VisualizationObjects.Count() > 1; + foreach (var visualizationObject in visualizationPanel.VisualizationObjects) { - viewEnumerator.MoveNext(); - this.AddContextMenuItems(viewEnumerator.Current, false); - } - else - { - while (viewEnumerator.MoveNext()) + ItemsControl root = this.ContextMenu; + + // If we're adding a cascading menu + if (addVisualizationObjectCommandsInSubmenus) { - this.AddContextMenuItems(viewEnumerator.Current, true); + // Then add the top level of the cascading menu to the main context menu and set the root + // to it instead. + if (root.Items.Count > 0) + { + root.Items.Add(new Separator()); + } + + var newRoot = MenuItemHelper.CreateMenuItem(IconSourcePath.Stream, visualizationObject.Name, null); + root.Items.Add(newRoot); + root = newRoot; } + + this.AddContextMenuItems(root, visualizationObject.ContextMenuItemsInfo()); } } - // Add the context menu items for the visualization panel, instant panel, and visualization conatiner. - foreach (IContextMenuItemsSource panelViewSource in this.contextMenuSources.Values) + // Add the context menu items for the visualization panel, instant panel, and visualization container. + foreach (var contextMenuItemSource in this.contextMenuItemsSources) { - this.AddContextMenuItems(panelViewSource, false); + this.AddContextMenuItems(this.ContextMenu, contextMenuItemSource.ContextMenuItemsInfo()); } } @@ -335,76 +318,82 @@ private HitTestFilterBehavior ContestMenuHitTestFilter(DependencyObject dependen // Return the result of the hit test to the callback. private HitTestResultBehavior ContextMenuHitTestResult(HitTestResult result) { - DependencyObject dependencyObject = result.VisualHit; + var dependencyObject = result.VisualHit; while (dependencyObject != null) { - // If the dependency object is not a hidden panel - if (!(dependencyObject is VisualizationPanelView visualizationPanelView && !(visualizationPanelView.DataContext as VisualizationPanel).IsShown)) + // If the dependency object is a framework element + if (dependencyObject is FrameworkElement frameworkElement) { - // If the object is a context menu item source and not a visualization object view, add it to the collection. - // (The context menu items for the visualization object views will be collected later from the panel) - if (dependencyObject is IContextMenuItemsSource contextMenuSource && contextMenuSource.ContextMenuItemsSourceType != ContextMenuItemsSourceType.VisualizationObject) + // Optimization: If we have reached the visualization container view level, + // we can stop since it's the top level visual we care about. + if (frameworkElement is VisualizationContainerView) { - this.contextMenuSources[contextMenuSource.ContextMenuItemsSourceType] = contextMenuSource; + break; } - } - // Optimization: If we're at the visualization container view level, - // we can stop since it's the top level visual we care about. - if (dependencyObject is VisualizationContainerView) - { - break; - } - else - { - dependencyObject = VisualTreeHelper.GetParent(dependencyObject); + // If the dependency object is not a hidden panel + if (!(frameworkElement is VisualizationPanelView visualizationPanelView && !(visualizationPanelView.DataContext as VisualizationPanel).IsShown)) + { + // If the corresponding data context is a context menu item source and not a + // visualization object view, then add it to the collection. The context menu + // items for visualization objects will be collected later from the + // visualization panel. + if (frameworkElement.DataContext is IContextMenuItemsSource contextMenuItemsSource && + frameworkElement.DataContext is not VisualizationObject) + { + // If the source has not been already added + if (!this.contextMenuItemsSources.Contains(contextMenuItemsSource)) + { + // Add the source to the set of sources + this.contextMenuItemsSources.Add(contextMenuItemsSource); + } + } + } } + + // Traverse up the tree + dependencyObject = VisualTreeHelper.GetParent(dependencyObject); } // Set the behavior to return visuals at all z-order levels. return HitTestResultBehavior.Continue; } - private void AddContextMenuItems(IContextMenuItemsSource menuItemSource, bool addAsCascadingMenu) + private void AddContextMenuItems(ItemsControl itemsControl, List commands) { - // Assume the menu root is the main context menu. - ItemsControl root = this.ContextMenu; - - // If we're adding a cascading menu, add the top level of the cascading - // menu to the main context menu and set the root to it instead. - if (addAsCascadingMenu) - { - if (root.Items.Count > 0) - { - root.Items.Add(new Separator()); - } - - ItemsControl newRoot = MenuItemHelper.CreateMenuItem(IconSourcePath.Stream, menuItemSource.ContextMenuObjectName, null); - root.Items.Add(newRoot); - root = newRoot; - } - - // Get the list of context menu items from the context menu items source - var menuItems = new List(); - menuItemSource.AppendContextMenuItems(menuItems); - // Add the context menu items to the context menu root. - if (menuItems != null && menuItems.Any()) + if (commands != null && commands.Any()) { - if (root.Items.Count > 0) + if (itemsControl.Items.Count > 0) { - root.Items.Add(new Separator()); + itemsControl.Items.Add(new Separator()); } - foreach (var menuItem in menuItems) + foreach (var command in commands) { - if (menuItem != null) + if (command != null) { - root.Items.Add(menuItem); + if (command.HasSubItems) + { + var subMenu = MenuItemHelper.CreateMenuItem(null, command.DisplayName, null); + itemsControl.Items.Add(subMenu); + this.AddContextMenuItems(subMenu, command.SubItems); + } + else + { + itemsControl.Items.Add( + MenuItemHelper.CreateMenuItem( + command.IconSourcePath, + command.DisplayName, + command.Command, + command.Tag, + command.IsEnabled, + command.CommandParameter)); + } } else { - root.Items.Add(new Separator()); + itemsControl.Items.Add(new Separator()); } } } diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/VisualizationObjectView.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/VisualizationObjectView.cs index 2f993d475..f56ba363e 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/VisualizationObjectView.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/VisualizationObjectView.cs @@ -4,18 +4,16 @@ namespace Microsoft.Psi.Visualization.Views { using System; - using System.Collections.Generic; using System.ComponentModel; using System.Windows; using System.Windows.Controls; - using Microsoft.Psi.Visualization.Helpers; using Microsoft.Psi.Visualization.Navigation; using Microsoft.Psi.Visualization.VisualizationObjects; /// /// Provides an abstract base class for visualization object views. /// - public abstract class VisualizationObjectView : UserControl, IContextMenuItemsSource + public abstract class VisualizationObjectView : UserControl { /// /// Initializes a new instance of the class. @@ -27,12 +25,6 @@ public VisualizationObjectView() this.Unloaded += this.OnUnloaded; } - /// - public ContextMenuItemsSourceType ContextMenuItemsSourceType => ContextMenuItemsSourceType.VisualizationObject; - - /// - public string ContextMenuObjectName => this.DataContext is VisualizationObject visualizationObject ? visualizationObject.Name : string.Empty; - /// /// Gets the visualization object. /// @@ -43,43 +35,6 @@ public VisualizationObjectView() /// public Navigator Navigator => this.VisualizationObject.Navigator; - /// - public virtual void AppendContextMenuItems(List menuItems) - { - if (this.DataContext is VisualizationObject visualizationObject) - { - // If the visualization object is bound and allows snapping to its stream, add the snap to stream menuitem. - if (visualizationObject is IStreamVisualizationObject streamVisualizationObject && streamVisualizationObject.IsBound) - { - if (visualizationObject.CanSnapToStream) - { - menuItems.Add(MenuItemHelper.CreateMenuItem( - IconSourcePath.SnapToStream, - visualizationObject.IsSnappedToStream ? $"Unsnap from Stream" : $"Snap to Stream", - new VisualizationCommand(() => visualizationObject.ToggleSnapToStream()))); - } - } - - // Add the show/hide menuitem. - menuItems.Add(MenuItemHelper.CreateMenuItem( - IconSourcePath.ToggleVisibility, - visualizationObject.Visible ? "Hide Visualizer" : "Show Visualizers", - visualizationObject.ToggleVisibilityCommand, - null, - true, - null)); - - // Add the remove from panel menuitem. - menuItems.Add(MenuItemHelper.CreateMenuItem( - IconSourcePath.RemovePanel, - $"Remove Visualizer", - visualizationObject.Panel.DeleteVisualizationCommand, - null, - true, - visualizationObject)); - } - } - /// /// Called when the data context is changed. /// diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/VisualizationPanelView.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/VisualizationPanelView.cs index 3f9387085..0d2a181e9 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/VisualizationPanelView.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/VisualizationPanelView.cs @@ -3,81 +3,12 @@ namespace Microsoft.Psi.Visualization.Views { - using System.Collections.Generic; - using System.Linq; using System.Windows.Controls; - using Microsoft.Psi.Visualization.Helpers; - using Microsoft.Psi.Visualization.VisualizationPanels; /// /// Represents the base class for all visualization panel views. /// - public abstract class VisualizationPanelView : UserControl, IContextMenuItemsSource + public abstract class VisualizationPanelView : UserControl { - /// - public ContextMenuItemsSourceType ContextMenuItemsSourceType => ContextMenuItemsSourceType.VisualizationPanel; - - /// - public string ContextMenuObjectName => string.Empty; - - /// - public virtual void AppendContextMenuItems(List menuItems) - { - if (this.DataContext is VisualizationPanel visualizationPanel) - { - if (visualizationPanel.VisualizationObjects.Count > 0) - { - var visible = visualizationPanel.VisualizationObjects.Any(vo => vo.Visible); - menuItems.Add( - MenuItemHelper.CreateMenuItem( - IconSourcePath.ToggleVisibility, - visible ? "Hide All Visualizers" : "Show All Visualizers", - visualizationPanel.ToggleAllVisualizersVisibilityCommand, - null, - true)); - } - - menuItems.Add( - MenuItemHelper.CreateMenuItem( - IconSourcePath.ClearPanel, - $"Remove All Visualizers", - visualizationPanel.ClearPanelCommand, - null, - visualizationPanel.VisualizationObjects.Count > 0)); - - // Add copy to clipboard menu with sub-menu items - var copyToClipboardMenuItem = MenuItemHelper.CreateMenuItem( - string.Empty, - "Copy to Clipboard", - null); - - copyToClipboardMenuItem.Items.Add( - MenuItemHelper.CreateMenuItem( - null, - "Cursor Time", - visualizationPanel.Navigator.CopyToClipboardCommand, - null, - true, - visualizationPanel.Navigator.Cursor.ToString("M/d/yyyy HH:mm:ss.ffff"))); - copyToClipboardMenuItem.Items.Add( - MenuItemHelper.CreateMenuItem( - null, - "Session Name & Cursor Time", - visualizationPanel.Navigator.CopyToClipboardCommand, - null, - VisualizationContext.Instance.DatasetViewModel?.CurrentSessionViewModel != null, - VisualizationContext.Instance.DatasetViewModel.CurrentSessionViewModel.Name.ToString() + "@" + visualizationPanel.Navigator.Cursor.ToString("M/d/yyyy HH:mm:ss.ffff"))); - - menuItems.Add(copyToClipboardMenuItem); - - menuItems.Add( - MenuItemHelper.CreateMenuItem( - null, - $"Go To Time ...", - visualizationPanel.Container.GoToTimeCommand, - null, - true)); - } - } } } diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals2D/AudioVisualizationObjectView.xaml.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals2D/AudioVisualizationObjectView.xaml.cs index 5b5c99443..05a3520d4 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals2D/AudioVisualizationObjectView.xaml.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals2D/AudioVisualizationObjectView.xaml.cs @@ -3,9 +3,6 @@ namespace Microsoft.Psi.Visualization.Views.Visuals2D { - using System.Collections.Generic; - using System.Windows.Controls; - using Microsoft.Psi.Visualization.Helpers; using Microsoft.Psi.Visualization.VisualizationObjects; /// @@ -21,16 +18,5 @@ public AudioVisualizationObjectView() this.InitializeComponent(); this.Canvas = this._DynamicCanvas; } - - /// - public override void AppendContextMenuItems(List menuItems) - { - if (this.DataContext is AudioVisualizationObject audioVisualizationObject && audioVisualizationObject.IsBound) - { - menuItems.Add(MenuItemHelper.CreateMenuItem(audioVisualizationObject.ContextMenuIconSource, audioVisualizationObject.EnableAudioCommandText, audioVisualizationObject.EnableAudioCommand)); - } - - base.AppendContextMenuItems(menuItems); - } } } diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals2D/DepthImageVisualizationObjectView.xaml.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals2D/DepthImageVisualizationObjectView.xaml.cs index 7e321bf20..935d61524 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals2D/DepthImageVisualizationObjectView.xaml.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals2D/DepthImageVisualizationObjectView.xaml.cs @@ -3,15 +3,11 @@ namespace Microsoft.Psi.Visualization.Views.Visuals2D { - using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.Windows; - using System.Windows.Controls; - using GalaSoft.MvvmLight.CommandWpf; using Microsoft.Psi.Imaging; using Microsoft.Psi.Visualization; - using Microsoft.Psi.Visualization.Helpers; using Microsoft.Psi.Visualization.VisualizationObjects; /// @@ -37,39 +33,7 @@ public DepthImageVisualizationObjectView() /// /// Gets the depth image visualization object. /// - public DepthImageVisualizationObject DepthImageVisualizationObject => - this.VisualizationObject as DepthImageVisualizationObject; - - /// - public override void AppendContextMenuItems(List menuItems) - { - base.AppendContextMenuItems(menuItems); - - // Add Set Cursor Epsilon menu with sub-menu items - var rangeModeMenuItem = MenuItemHelper.CreateMenuItem( - string.Empty, - "Set Range Mode", - null, - true); - - rangeModeMenuItem.Items.Add( - MenuItemHelper.CreateMenuItem( - string.Empty, - DepthImageRangeMode.Auto.ToString(), - new RelayCommand( - () => this.DepthImageVisualizationObject.RangeMode = DepthImageRangeMode.Auto), - true)); - - rangeModeMenuItem.Items.Add( - MenuItemHelper.CreateMenuItem( - string.Empty, - DepthImageRangeMode.Maximum.ToString(), - new RelayCommand( - () => this.DepthImageVisualizationObject.RangeMode = DepthImageRangeMode.Maximum), - true)); - - menuItems.Add(rangeModeMenuItem); - } + public DepthImageVisualizationObject DepthImageVisualizationObject => this.VisualizationObject as DepthImageVisualizationObject; /// protected override void UpdateView() diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals2D/DiagnosticsVisualization/PipelineDiagnosticsVisualizationModel.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals2D/DiagnosticsVisualization/PipelineDiagnosticsVisualizationModel.cs index 31cf8bad4..951b0c455 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals2D/DiagnosticsVisualization/PipelineDiagnosticsVisualizationModel.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals2D/DiagnosticsVisualization/PipelineDiagnosticsVisualizationModel.cs @@ -12,7 +12,7 @@ namespace Microsoft.Psi.Visualization.Views.Visuals2D /// public partial class PipelineDiagnosticsVisualizationModel { - private Stack navStack = new Stack(); + private readonly Stack navStack = new (); /// /// Initializes a new instance of the class. diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals2D/DiagnosticsVisualization/PipelineDiagnosticsVisualizationObjectView.xaml.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals2D/DiagnosticsVisualization/PipelineDiagnosticsVisualizationObjectView.xaml.cs index d170347e6..dcbad21dc 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals2D/DiagnosticsVisualization/PipelineDiagnosticsVisualizationObjectView.xaml.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals2D/DiagnosticsVisualization/PipelineDiagnosticsVisualizationObjectView.xaml.cs @@ -13,7 +13,6 @@ namespace Microsoft.Psi.Visualization.Views.Visuals2D using Microsoft.Msagl.Drawing; using Microsoft.Msagl.WpfGraphControl; using Microsoft.Psi.Diagnostics; - using Microsoft.Psi.Visualization.Helpers; using Microsoft.Psi.Visualization.VisualizationObjects; using Brushes = System.Windows.Media.Brushes; using Transform = System.Windows.Media.Transform; @@ -23,8 +22,8 @@ namespace Microsoft.Psi.Visualization.Views.Visuals2D /// public partial class PipelineDiagnosticsVisualizationObjectView : VisualizationObjectView, IDisposable { - private readonly GraphViewer graphViewer = new GraphViewer() { LayoutEditingEnabled = false }; - private Dictionary graphVisualPanZoom = new Dictionary(); + private readonly GraphViewer graphViewer = new () { LayoutEditingEnabled = false }; + private Dictionary graphVisualPanZoom = new (); private Transform lastRenderTransform = Transform.Identity; private Node lastCenteredNode = null; private PipelineDiagnosticsVisualizationPresenter presenter; @@ -60,51 +59,6 @@ public void Update(bool forceRelayout) this.infoText.Text = this.presenter.SelectedEdgeDetails; } - /// - public override void AppendContextMenuItems(List menuItems) - { - // If the mouse is over an edge, add a menu item to expand the streams of the receiver. - if (this.graphViewer.ObjectUnderMouseCursor is VEdge vedge) - { - menuItems.Add( - MenuItemHelper.CreateMenuItem( - IconSourcePath.Diagnostics, - $"Add derived diagnostics streams for receiver {vedge.Edge.UserData}", - new PsiCommand(() => this.presenter.AddDerivedReceiverDiagnosticsStreams((int)vedge.Edge.UserData)))); - } - - // Add a heatmap statistics context menu - var heatmapStatisticsMenu = MenuItemHelper.CreateMenuItem(null, "Heatmap Statistics", null); - menuItems.Add(heatmapStatisticsMenu); - foreach (var heatmapStat in Enum.GetValues(typeof(PipelineDiagnosticsVisualizationObject.HeatmapStats))) - { - var heatmapStatValue = (PipelineDiagnosticsVisualizationObject.HeatmapStats)heatmapStat; - var heatmapStatName = heatmapStatValue switch - { - PipelineDiagnosticsVisualizationObject.HeatmapStats.None => "None", - PipelineDiagnosticsVisualizationObject.HeatmapStats.AvgMessageCreatedLatency => "Message Created Latency (Average)", - PipelineDiagnosticsVisualizationObject.HeatmapStats.AvgMessageEmittedLatency => "Message Emitted Latency (Average)", - PipelineDiagnosticsVisualizationObject.HeatmapStats.AvgMessageReceivedLatency => "Message Received Latency (Average)", - PipelineDiagnosticsVisualizationObject.HeatmapStats.AvgDeliveryQueueSize => "Delivery Queue Size (Average)", - PipelineDiagnosticsVisualizationObject.HeatmapStats.AvgMessageProcessTime => "Message Process Time (Average)", - PipelineDiagnosticsVisualizationObject.HeatmapStats.TotalMessageEmittedCount => "Total Messages Emitted (Count)", - PipelineDiagnosticsVisualizationObject.HeatmapStats.TotalMessageDroppedCount => "Total Messages Dropped (Count)", - PipelineDiagnosticsVisualizationObject.HeatmapStats.TotalMessageDroppedPercentage => "Total Messages Dropped (%)", - PipelineDiagnosticsVisualizationObject.HeatmapStats.TotalMessageProcessedCount => "Total Messages Processed (Count)", - PipelineDiagnosticsVisualizationObject.HeatmapStats.AvgMessageSize => "Message Size (Average)", - _ => throw new NotImplementedException(), - }; - - heatmapStatisticsMenu.Items.Add( - MenuItemHelper.CreateMenuItem( - this.PipelineDiagnosticsVisualizationObject.HeatmapStatistics == heatmapStatValue ? IconSourcePath.Checkmark : null, - heatmapStatName, - new PsiCommand(() => this.PipelineDiagnosticsVisualizationObject.HeatmapStatistics = heatmapStatValue))); - } - - base.AppendContextMenuItems(menuItems); - } - /// protected override void OnLoaded(object sender, RoutedEventArgs e) { @@ -160,6 +114,15 @@ protected override void OnVisualizationObjectPropertyChanged(object sender, Prop private void GraphViewer_ObjectUnderMouseCursorChanged(object sender, ObjectUnderMouseCursorChangedEventArgs e) { Mouse.OverrideCursor = e.NewObject != null ? Cursors.Hand : Cursors.Arrow; + + if (e.NewObject is VEdge edge) + { + this.PipelineDiagnosticsVisualizationObject.UpdateEdgeUnderCursor(Convert.ToInt32(edge.Edge.UserData)); + } + else + { + this.PipelineDiagnosticsVisualizationObject.UpdateEdgeUnderCursor(-1); + } } private void DiagnosticsVisualizationObjectView_SizeChanged(object sender, SizeChangedEventArgs e) diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals2D/DiagnosticsVisualization/PipelineDiagnosticsVisualizationPresenter.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals2D/DiagnosticsVisualization/PipelineDiagnosticsVisualizationPresenter.cs index ab3b9b9f8..734b63fc9 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals2D/DiagnosticsVisualization/PipelineDiagnosticsVisualizationPresenter.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals2D/DiagnosticsVisualization/PipelineDiagnosticsVisualizationPresenter.cs @@ -162,6 +162,15 @@ public void UpdateSettings(PipelineDiagnosticsVisualizationObject visualizationO /// Force re-layout of graph (otherwise, updates labels, colors, etc. in place). public void UpdateGraph(PipelineDiagnostics graph, bool forceRelayout) { + // If the model visualization object is dirty, rebuild + if (this.model.VisualizationObject.ModelDirty) + { + this.model.Reset(); + this.model.VisualizationObject.ModelDirty = false; + this.VisualGraph = null; + this.view.Update(true); + } + this.model.Graph = graph; if (graph == null) { @@ -207,12 +216,13 @@ public void UpdateReceiverDiagnostics(Edge edge) } /// - /// Adds the derived receiver diagnostics streams for a specified receiver id. + /// Adds a derived receiver diagnostics streams for a specified receiver id and receiver statistic. /// /// The receiver id. - public void AddDerivedReceiverDiagnosticsStreams(int receiverId) + /// The receiver diagnostics statistic. + public void AddDerivedReceiverDiagnosticsStreams(int receiverId, string receiverDiagnosticsStatistic) { - this.model.VisualizationObject.AddDerivedReceiverDiagnosticsStreams(receiverId); + this.model.VisualizationObject.AddReceiverDiagnosticsDerivedStream(receiverId, receiverDiagnosticsStatistic); } /// diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals2D/NumericPlot/PlotSeriesVisualizationObjectView.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals2D/NumericPlot/PlotSeriesVisualizationObjectView.cs index a0af8846c..ed8b6bf62 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals2D/NumericPlot/PlotSeriesVisualizationObjectView.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals2D/NumericPlot/PlotSeriesVisualizationObjectView.cs @@ -8,12 +8,10 @@ namespace Microsoft.Psi.Visualization.Views.Visuals2D using System.Collections.Specialized; using System.ComponentModel; using System.Linq; - using System.Windows.Controls; using System.Windows.Data; using System.Windows.Media; using System.Windows.Shapes; using Microsoft.Psi.Visualization.Data; - using Microsoft.Psi.Visualization.Helpers; using Microsoft.Psi.Visualization.Navigation; using Microsoft.Psi.Visualization.VisualizationObjects; @@ -34,7 +32,7 @@ public class PlotSeriesVisualizationObjectView, new() { private readonly PlotVisualizationObjectViewHelper helper; - private readonly Dictionary seriesKeyColorPaletteIndex = new Dictionary(); + private readonly Dictionary seriesKeyColorPaletteIndex = new (); /// /// Initializes a new instance of the class. @@ -62,30 +60,6 @@ public TPlotSeriesVisualizationObject PlotSeriesVisualizationObject /// public double MarkerSize => this.PlotSeriesVisualizationObject.MarkerSize; - /// - public override void AppendContextMenuItems(List menuItems) - { - base.AppendContextMenuItems(menuItems); - - if (this.DataContext is PlotSeriesVisualizationObject plotSeriesVisualizationObject) - { - if (plotSeriesVisualizationObject.MarkerStyle == MarkerStyle.None) - { - menuItems.Add(MenuItemHelper.CreateMenuItem( - null, - "Show Markers", - new VisualizationCommand(() => plotSeriesVisualizationObject.MarkerStyle = MarkerStyle.Circle))); - } - else - { - menuItems.Add(MenuItemHelper.CreateMenuItem( - null, - "Hide Markers", - new VisualizationCommand(() => plotSeriesVisualizationObject.MarkerStyle = MarkerStyle.None))); - } - } - } - /// public void CreateBindings(TKey seriesKey, Path linePath, Path markerPath, Path rangePath) { diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals2D/NumericPlot/PlotVisualizationObjectView.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals2D/NumericPlot/PlotVisualizationObjectView.cs index 2c723c414..70ebd6057 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals2D/NumericPlot/PlotVisualizationObjectView.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals2D/NumericPlot/PlotVisualizationObjectView.cs @@ -8,13 +8,11 @@ namespace Microsoft.Psi.Visualization.Views.Visuals2D using System.Collections.Specialized; using System.ComponentModel; using System.Linq; - using System.Windows.Controls; using System.Windows.Data; using System.Windows.Media; using System.Windows.Shapes; using Microsoft.Psi.Visualization; using Microsoft.Psi.Visualization.Data; - using Microsoft.Psi.Visualization.Helpers; using Microsoft.Psi.Visualization.Navigation; using Microsoft.Psi.Visualization.VisualizationObjects; @@ -60,30 +58,6 @@ public PlotVisualizationObjectView() /// public double MarkerSize => this.PlotVisualizationObject.MarkerSize; - /// - public override void AppendContextMenuItems(List menuItems) - { - base.AppendContextMenuItems(menuItems); - - if (this.DataContext is PlotVisualizationObject plotVisualizationObject) - { - if (plotVisualizationObject.MarkerStyle == MarkerStyle.None) - { - menuItems.Add(MenuItemHelper.CreateMenuItem( - null, - "Show Markers", - new VisualizationCommand(() => plotVisualizationObject.MarkerStyle = MarkerStyle.Circle))); - } - else - { - menuItems.Add(MenuItemHelper.CreateMenuItem( - null, - "Hide Markers", - new VisualizationCommand(() => plotVisualizationObject.MarkerStyle = MarkerStyle.None))); - } - } - } - /// public void CreateBindings(int seriesKey, Path linePath, Path markerPath, Path rangePath) { diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals2D/TimeIntervalAnnotationVisualizationObjectView.xaml.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals2D/TimeIntervalAnnotationVisualizationObjectView.xaml.cs index cead0b79a..76d259693 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals2D/TimeIntervalAnnotationVisualizationObjectView.xaml.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals2D/TimeIntervalAnnotationVisualizationObjectView.xaml.cs @@ -46,55 +46,6 @@ public TimeIntervalAnnotationVisualizationObjectView() this.Canvas.Children.Add(this.trackHighlight); } - /// - public override void AppendContextMenuItems(List menuItems) - { - if (this.DataContext is TimeIntervalAnnotationVisualizationObject annotationVisualizationObject && annotationVisualizationObject.IsBound) - { - // Get the track under the cursor - var trackUnderCursor = annotationVisualizationObject.GetTrackByIndex(this.GetTrackIndexUnderMouseCursor()); - - // Add the add annotation (on current track) context menu item - var addAnnotationOnCurrentTrackCommand = annotationVisualizationObject.GetAddAnnotationOnTrackCommand(trackUnderCursor); - menuItems.Add(MenuItemHelper.CreateMenuItem(IconSourcePath.Annotation, "Add Annotation", addAnnotationOnCurrentTrackCommand, null, addAnnotationOnCurrentTrackCommand != null)); - - // Add the add annotation on new track context menu item - var addAnnotationOnNewTrackCommand = annotationVisualizationObject.GetAddAnnotationOnNewTrackCommand(); - menuItems.Add(MenuItemHelper.CreateMenuItem(null, "Add Annotation on New Track", addAnnotationOnNewTrackCommand, null, addAnnotationOnNewTrackCommand != null)); - - // Add the delete annotation context menu item - var deleteCommand = annotationVisualizationObject.GetDeleteAnnotationOnTrackCommand(trackUnderCursor); - menuItems.Add(MenuItemHelper.CreateMenuItem(null, "Delete Annotation", deleteCommand, null, deleteCommand != null)); - - // Add the delete annotation context menu item - var deleteAllAnnotationsOnTrackCommand = annotationVisualizationObject.GetDeleteAllAnnotationsOnTrackCommand(trackUnderCursor); - menuItems.Add(MenuItemHelper.CreateMenuItem(null, $"Delete All Annotations on Curent Track ({trackUnderCursor})", deleteAllAnnotationsOnTrackCommand, null, deleteAllAnnotationsOnTrackCommand != null)); - - // Add the delete annotation context menu item - var renameCurrentTrackCommand = annotationVisualizationObject.GetRenameTrackCommand(trackUnderCursor); - menuItems.Add(MenuItemHelper.CreateMenuItem(null, $"Rename Curent Track ({trackUnderCursor})", renameCurrentTrackCommand, null, renameCurrentTrackCommand != null)); - - // Add the command to show all tracks - var showAllTracksCommand = annotationVisualizationObject.GetShowAllTracksCommand(); - if (showAllTracksCommand != null) - { - menuItems.Add(MenuItemHelper.CreateMenuItem(null, $"Show All Tracks", showAllTracksCommand, null, true)); - } - - // Add the command to show only tracks with events in view - var showOnlyTracksWithEventsInViewCommand = annotationVisualizationObject.GetShowOnlyTracksWithEventsInViewCommand(); - if (showOnlyTracksWithEventsInViewCommand != null) - { - menuItems.Add(MenuItemHelper.CreateMenuItem(null, $"Show Only Tracks with Events in View", showOnlyTracksWithEventsInViewCommand, null, true)); - } - - // Add a separator - menuItems.Add(null); - } - - base.AppendContextMenuItems(menuItems); - } - /// /// Returns a brush with the requested System.Drawing.Color from the brushes cache. /// @@ -138,7 +89,8 @@ protected override void OnVisualizationObjectPropertyChanged(object sender, Prop e.PropertyName == nameof(this.StreamVisualizationObject.LineWidth) || e.PropertyName == nameof(this.StreamVisualizationObject.Padding) || e.PropertyName == nameof(this.StreamVisualizationObject.FontSize) || - e.PropertyName == nameof(this.StreamVisualizationObject.ShowOnlyTracksWithEventsInView)) + e.PropertyName == nameof(this.StreamVisualizationObject.ShowTracks) || + e.PropertyName == nameof(this.StreamVisualizationObject.ShowTracksSelection)) { this.OnDisplayDataChanged(); } @@ -173,8 +125,9 @@ protected override void UpdateView() { if (i >= this.itemViews.Count) { - this.itemViews.Add( - new TimeIntervalAnnotationVisualizationObjectViewItem(this, this.StreamVisualizationObject.DisplayData[i])); + var item = new TimeIntervalAnnotationVisualizationObjectViewItem(this, this.StreamVisualizationObject.DisplayData[i]); + item.Update(this.StreamVisualizationObject.DisplayData[i], this.StreamVisualizationObject.DisplayData[i].TrackIndex, this.StreamIntervalVisualizationObject.TrackCount); + this.itemViews.Add(item); } else { @@ -185,11 +138,11 @@ protected override void UpdateView() // remove the remaining figures for (int i = this.StreamVisualizationObject.DisplayData.Count; i < this.itemViews.Count; i++) { - var item = this.itemViews[this.StreamVisualizationObject.DisplayData.Count]; - item.RemoveFromCanvas(); - this.itemViews.Remove(item); + this.itemViews[i].RemoveFromCanvas(); } + this.itemViews.RemoveRange(this.StreamVisualizationObject.DisplayData.Count, this.itemViews.Count - this.StreamVisualizationObject.DisplayData.Count); + // Go through the track views and create new ones where necessary for (int i = 0; i < this.StreamVisualizationObject.TrackCount; i++) { @@ -207,10 +160,10 @@ protected override void UpdateView() // remove the remaining figures for (int i = this.StreamVisualizationObject.TrackCount; i < this.trackViews.Count; i++) { - var trackView = this.trackViews[this.StreamVisualizationObject.TrackCount]; - trackView.RemoveFromCanvas(); - this.trackViews.Remove(trackView); + this.trackViews[i].RemoveFromCanvas(); } + + this.trackViews.RemoveRange(this.StreamVisualizationObject.TrackCount, this.trackViews.Count - this.StreamVisualizationObject.TrackCount); } private static Color ToMediaColor(System.Drawing.Color color) @@ -218,7 +171,9 @@ private static Color ToMediaColor(System.Drawing.Color color) private void UpdateTrackHighlight() { - if (this.DataContext is TimeIntervalAnnotationVisualizationObject annotationVisualizationObject && annotationVisualizationObject.IsBound) + if (this.DataContext is TimeIntervalAnnotationVisualizationObject annotationVisualizationObject && + annotationVisualizationObject.IsBound && + annotationVisualizationObject.TrackCount > 0) { this.trackHighlight.Background = new SolidColorBrush(Color.FromRgb(0x2D, 0x2D, 0x30)); this.trackHighlight.Width = this.Canvas.ActualWidth; @@ -351,12 +306,6 @@ private TimelineVisualizationPanelView FindTimelineVisualizationPanelView() return timelinePanelView as TimelineVisualizationPanelView; } - private int GetTrackIndexUnderMouseCursor() - { - var visualizationObject = this.DataContext as TimeIntervalAnnotationVisualizationObject; - return (int)(Mouse.GetPosition(this.Canvas).Y * visualizationObject.TrackCount / this.Canvas.ActualHeight); - } - private class UnrestrictedAnnotationValueContext { public UnrestrictedAnnotationValueContext(TimeIntervalAnnotationDisplayData displayData, string attributeName) diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals2D/TimeIntervalAnnotationVisualizationObjectViewItem.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals2D/TimeIntervalAnnotationVisualizationObjectViewItem.cs index 8ef87f69b..93e114560 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals2D/TimeIntervalAnnotationVisualizationObjectViewItem.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals2D/TimeIntervalAnnotationVisualizationObjectViewItem.cs @@ -155,6 +155,25 @@ public bool IsSelected /// The track count. internal void Update(TimeIntervalAnnotationDisplayData annotationDisplayData, int trackIndex, int trackCount) { + // First, check if the annotation display data is in view + var notInView = annotationDisplayData.EndTime < this.parent.Navigator.ViewRange.StartTime || + annotationDisplayData.StartTime > this.parent.Navigator.ViewRange.EndTime; + + // If not in view + if (notInView) + { + // Collapse its elements + for (int attributeIndex = 0; attributeIndex < annotationDisplayData.AnnotationSchema.AttributeSchemas.Count; attributeIndex++) + { + this.figures[attributeIndex].Visibility = Visibility.Collapsed; + this.labels[attributeIndex].Visibility = Visibility.Collapsed; + this.borderPath.Visibility = Visibility.Collapsed; + } + + // And return + return; + } + var verticalSpace = this.parent.StreamVisualizationObject.Padding / this.parent.ScaleTransform.ScaleY; var start = (annotationDisplayData.StartTime - this.parent.Navigator.DataRange.StartTime).TotalSeconds; var end = (annotationDisplayData.EndTime - this.parent.Navigator.DataRange.StartTime).TotalSeconds; @@ -169,6 +188,18 @@ internal void Update(TimeIntervalAnnotationDisplayData annotationDisplayData, in for (int attributeIndex = 0; attributeIndex < annotationDisplayData.AnnotationSchema.AttributeSchemas.Count; attributeIndex++) { + // Set visibility for the figure + if (this.figures[attributeIndex].Visibility != Visibility.Visible) + { + this.figures[attributeIndex].Visibility = Visibility.Visible; + } + + // Set visibility for the label + if (this.labels[attributeIndex].Visibility != Visibility.Visible) + { + this.labels[attributeIndex].Visibility = Visibility.Visible; + } + // Get the attribute schema var attributeSchema = annotationDisplayData.AnnotationSchema.AttributeSchemas[attributeIndex]; @@ -176,36 +207,118 @@ internal void Update(TimeIntervalAnnotationDisplayData annotationDisplayData, in var annotationValue = annotationDisplayData.Annotation.AttributeValues[attributeSchema.Name]; // Set the colors etc - this.figures[attributeIndex].Fill = this.parent.GetBrush(annotationValue.FillColor); + if (this.figures[attributeIndex].Fill != this.parent.GetBrush(annotationValue.FillColor)) + { + this.figures[attributeIndex].Fill = this.parent.GetBrush(annotationValue.FillColor); + } var lo = (double)(trackIndex * attributeCount + attributeIndex + verticalSpace) / totalTrackCount; var hi = (double)(trackIndex * attributeCount + attributeIndex + 1 - verticalSpace) / totalTrackCount; var annotationElementFigure = (this.figures[attributeIndex].Data as PathGeometry).Figures[0]; - annotationElementFigure.StartPoint = new Point(start, lo); - (annotationElementFigure.Segments[0] as LineSegment).Point = new Point(end, lo); - (annotationElementFigure.Segments[1] as LineSegment).Point = new Point(end, hi); - (annotationElementFigure.Segments[2] as LineSegment).Point = new Point(start, hi); + if (annotationElementFigure.StartPoint.X != start || + annotationElementFigure.StartPoint.Y != end || + (annotationElementFigure.Segments[0] as LineSegment).Point.Y != lo || + (annotationElementFigure.Segments[1] as LineSegment).Point.Y != hi) + { + annotationElementFigure.StartPoint = new Point(start, lo); + (annotationElementFigure.Segments[0] as LineSegment).Point = new Point(end, lo); + (annotationElementFigure.Segments[1] as LineSegment).Point = new Point(end, hi); + (annotationElementFigure.Segments[2] as LineSegment).Point = new Point(start, hi); + } var labelGrid = this.labels[attributeIndex]; - (labelGrid.Children[0] as TextBlock).Text = annotationValue.ValueAsString; - (labelGrid.Children[0] as TextBlock).FontSize = this.parent.StreamVisualizationObject.FontSize; - (labelGrid.Children[0] as TextBlock).Foreground = this.parent.GetBrush(annotationValue.TextColor); - - labelGrid.Width = (labelEnd - labelStart) * this.parent.Canvas.ActualWidth / this.parent.Navigator.ViewRange.Duration.TotalSeconds; - labelGrid.Height = (hi - lo) * this.parent.Canvas.ActualHeight; - (labelGrid.RenderTransform as TranslateTransform).X = labelStart * this.parent.Canvas.ActualWidth / this.parent.Navigator.ViewRange.Duration.TotalSeconds; - (labelGrid.RenderTransform as TranslateTransform).Y = lo * this.parent.Canvas.ActualHeight; + var labelWidth = (int)((labelEnd - labelStart) * this.parent.Canvas.ActualWidth / this.parent.Navigator.ViewRange.Duration.TotalSeconds); + var labelHeight = (int)((hi - lo) * this.parent.Canvas.ActualHeight); + + // Measure how large the label should be, and if we don't have enough space, + // activate a tooltip + var t = new TextBlock() + { + IsHitTestVisible = false, + Text = annotationValue.ValueAsString, + }; + t.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); + if (t.DesiredSize.Width > labelWidth || t.DesiredSize.Height > labelHeight) + { + this.figures[attributeIndex].ToolTip = annotationValue.ValueAsString; + } + else + { + this.figures[attributeIndex].ToolTip = null; + } + + // If the label is in view and large enough + if (labelEnd > 0 && labelStart < navigatorViewDuration && labelWidth > 20) + { + labelGrid.Visibility = Visibility.Visible; + if (labelGrid.Width != labelWidth) + { + labelGrid.Width = labelWidth; + } + + if (labelGrid.Height != labelHeight) + { + labelGrid.Height = labelHeight; + } + + var textBlock = labelGrid.Children[0] as TextBlock; + if (textBlock.Text != annotationValue.ValueAsString) + { + textBlock.Text = annotationValue.ValueAsString; + } + + if (textBlock.FontSize != this.parent.StreamVisualizationObject.FontSize) + { + textBlock.FontSize = this.parent.StreamVisualizationObject.FontSize; + } + + if (textBlock.Foreground != this.parent.GetBrush(annotationValue.TextColor)) + { + textBlock.Foreground = this.parent.GetBrush(annotationValue.TextColor); + } + + var transformX = (int)(labelStart * this.parent.Canvas.ActualWidth / this.parent.Navigator.ViewRange.Duration.TotalSeconds); + var transformY = (int)(lo * this.parent.Canvas.ActualHeight); + var translateTranform = labelGrid.RenderTransform as TranslateTransform; + + if (translateTranform.X != transformX) + { + translateTranform.X = transformX; + } + + if (translateTranform.Y != transformY) + { + translateTranform.Y = transformY; + } + } + else + { + labelGrid.Visibility = Visibility.Collapsed; + } + } + + // Set visibility for the border + if (this.borderPath.Visibility != Visibility.Visible) + { + this.borderPath.Visibility = Visibility.Visible; } var borderLo = (double)(trackIndex * attributeCount + 0 + verticalSpace) / totalTrackCount; var borderHi = (double)(trackIndex * attributeCount + attributeCount - verticalSpace) / totalTrackCount; var borderFigure = (this.borderPath.Data as PathGeometry).Figures[0]; - borderFigure.StartPoint = new Point(start, borderLo); - (borderFigure.Segments[0] as LineSegment).Point = new Point(end, borderLo); - (borderFigure.Segments[1] as LineSegment).Point = new Point(end, borderHi); - (borderFigure.Segments[2] as LineSegment).Point = new Point(start, borderHi); + + if (borderFigure.StartPoint.X != start || + borderFigure.StartPoint.Y != borderLo || + (borderFigure.Segments[0] as LineSegment).Point.X != end || + (borderFigure.Segments[1] as LineSegment).Point.Y != borderHi) + { + borderFigure.StartPoint = new Point(start, borderLo); + (borderFigure.Segments[0] as LineSegment).Point = new Point(end, borderLo); + (borderFigure.Segments[1] as LineSegment).Point = new Point(end, borderHi); + (borderFigure.Segments[2] as LineSegment).Point = new Point(start, borderHi); + } } /// diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals3D/AnimatedModelVisual.xaml b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals3D/AnimatedModelVisual.xaml deleted file mode 100644 index 8fe01edc8..000000000 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals3D/AnimatedModelVisual.xaml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals3D/AnimatedModelVisual.xaml.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals3D/AnimatedModelVisual.xaml.cs deleted file mode 100644 index 2ad09688b..000000000 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/Visuals3D/AnimatedModelVisual.xaml.cs +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. - -namespace Microsoft.Psi.Visualization.Views.Visuals3D -{ - using System.ComponentModel; - using System.Windows.Media.Media3D; - using Microsoft.Psi.Visualization.Extensions; - using Microsoft.Psi.Visualization.VisualizationObjects; - - /// - /// Interaction logic for AnimatedModelVisual.xaml. - /// - public partial class AnimatedModelVisual : ModelVisual3D - { - private AnimatedModel3DVisualizationObject visualizationObject; - - /// - /// Initializes a new instance of the class. - /// - /// The animated model 3D visualization object. - public AnimatedModelVisual(AnimatedModel3DVisualizationObject visualizationObject) - { - this.InitializeComponent(); - this.LoadModel(); - this.visualizationObject = visualizationObject; - this.visualizationObject.PropertyChanged += this.VisualizationObject_PropertyChanged; - } - - /// - /// Gets a value indicating whether this is a camera location. - /// - public bool IsCameraLocation => this.visualizationObject.CameraTransform != null; - - /// - /// Gets the camera transformation. - /// - public Matrix3D CameraTransform => this.visualizationObject.CameraTransform.GetMatrix3D(); - - private void VisualizationObject_PropertyChanged(object sender, PropertyChangedEventArgs e) - { - if (e.PropertyName == nameof(this.visualizationObject.CurrentValue)) - { - var data = this.visualizationObject.CurrentValue.GetValueOrDefault().Data; - if (data != null) - { - this.Transform = new MatrixTransform3D(data.GetMatrix3D()); - } - } - else if (e.PropertyName == nameof(this.visualizationObject.Source)) - { - this.LoadModel(); - } - } - - private void LoadModel() - { - lock (this) - { - if (this.visualizationObject?.Source != null && this.root.Content == null) - { - HelixToolkit.Wpf.StLReader reader = new HelixToolkit.Wpf.StLReader(); - var model = reader.Read(this.visualizationObject.Source); - model.Freeze(); - this.root.Content = model; - } - } - } - - private void ChangeMaterials(Model3DCollection children, Material material) - { - foreach (var child in children) - { - if (child is GeometryModel3D) - { - ((GeometryModel3D)child).Material = material; - } - - if (child is Model3DGroup) - { - this.ChangeMaterials(((Model3DGroup)child).Children, material); - } - } - } - } -} diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/XYVisualizationPanelView.xaml.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/XYVisualizationPanelView.xaml.cs index 6c6ffeb26..0b913bf8c 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/XYVisualizationPanelView.xaml.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Views/XYVisualizationPanelView.xaml.cs @@ -3,10 +3,6 @@ namespace Microsoft.Psi.Visualization.Views { - using System.Collections.Generic; - using System.Windows.Controls; - using GalaSoft.MvvmLight.CommandWpf; - using Microsoft.Psi.Visualization.Helpers; using Microsoft.Psi.Visualization.VisualizationPanels; /// @@ -26,18 +22,5 @@ public XYVisualizationPanelView() /// Gets the visualization panel. /// protected XYVisualizationPanel VisualizationPanel => (XYVisualizationPanel)this.DataContext; - - /// - public override void AppendContextMenuItems(List menuItems) - { - menuItems.Add(MenuItemHelper.CreateMenuItem( - null, - "Auto-Fit Axes", - this.VisualizationPanel.SetAutoAxisComputeModeCommand, - null, - this.VisualizationPanel.AxisComputeMode == AxisComputeMode.Manual)); - - base.AppendContextMenuItems(menuItems); - } } } diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationContext.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationContext.cs index c594ab1e1..62ccaf815 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationContext.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationContext.cs @@ -12,6 +12,7 @@ namespace Microsoft.Psi.Visualization using System.Windows.Threading; using Microsoft.Psi.Data; using Microsoft.Psi.Persistence; + using Microsoft.Psi.Visualization.Adapters; using Microsoft.Psi.Visualization.Data; using Microsoft.Psi.Visualization.Navigation; using Microsoft.Psi.Visualization.ViewModels; @@ -22,7 +23,7 @@ namespace Microsoft.Psi.Visualization /// /// Data context for visualization. /// - public class VisualizationContext : ObservableObject + public class VisualizationContext : ObservableObject, IDisposable { private readonly DispatcherTimer liveStatusTimer = null; @@ -78,11 +79,7 @@ public DatasetViewModel DatasetViewModel public VisualizationContainer VisualizationContainer { get => this.visualizationContainer; - - set - { - this.Set(nameof(this.VisualizationContainer), ref this.visualizationContainer, value); - } + set => this.Set(nameof(this.VisualizationContainer), ref this.visualizationContainer, value); } /// @@ -95,13 +92,20 @@ public VisualizationContainer VisualizationContainer /// public string PlayPauseButtonToolTip => this.VisualizationContainer.Navigator.IsCursorModePlayback ? @"Stop" : @"Play"; + /// + public void Dispose() + { + this.VisualizationContainer?.Dispose(); + } + /// /// Opens a previously persisted layout file. /// /// The path to the layout to open. /// The name of the layout to open. + /// A flag indicating whether consent to apply this layout was explicitly given. This only applies to layouts containing scripts. /// True if the layout was successfully loaded, otherwise false. - public bool OpenLayout(string path, string name) + public bool OpenLayout(string path, string name, ref bool userConsented) { if (!string.IsNullOrWhiteSpace(path)) { @@ -109,26 +113,61 @@ public bool OpenLayout(string path, string name) VisualizationContainer newVisualizationContainer = VisualizationContainer.Load(path, name, this.VisualizationContainer); if (newVisualizationContainer != null) { - // NOTE: If we unbind the current VOs before binding the VOs of the new visualization - // container we risk data layer objects being disposed of because they temporarily - // have no subscribers. To avoid this, we'll bind the VOs in the new visualization - // container before we unbind the VOs from the visualization container that's being - // replaced. This way we'll ensure the subscriber count never goes to zero for data - // layer objects that are used by both the old and the new visualization container. + // If the new layout contains scripts, seek confirmation from the user before binding to the data + // Check if the new visualization container contains any derived stream visualizers. + var derivedStreamVisualizationObjects = newVisualizationContainer.GetDerivedStreamVisualizationObjects(); + + // Checks whether the adapter is a ScriptAdapter or has a ScriptAdapter somewhere in its chain + static bool ContainsScriptAdapter(Type adapterType) + { + if (adapterType.IsGenericType) + { + var genericAdapterType = adapterType.GetGenericTypeDefinition(); + if (genericAdapterType == typeof(ScriptAdapter<,>)) + { + return true; + } + else if (genericAdapterType == typeof(ChainedStreamAdapter<,,,,>)) + { + var genericTypeParams = adapterType.GetGenericArguments(); + return ContainsScriptAdapter(genericTypeParams[4]) || ContainsScriptAdapter(genericTypeParams[3]); + } + } + + return false; + } + + // Check whether any of the VO bindings contain scripted streams, and display a warning if consent has not previously been granted + bool hasScripts = derivedStreamVisualizationObjects.Any(vo => ContainsScriptAdapter(vo.StreamBinding.DerivedStreamAdapterType)); + if (hasScripts && !userConsented) + { + var confirmationWindow = new ConfirmLayoutWindow(Application.Current.MainWindow, name); + userConsented = confirmationWindow.ShowDialog() ?? false; + } + + if (!hasScripts || userConsented) + { + // NOTE: If we unbind the current VOs before binding the VOs of the new visualization + // container we risk data layer objects being disposed of because they temporarily + // have no subscribers. To avoid this, we'll bind the VOs in the new visualization + // container before we unbind the VOs from the visualization container that's being + // replaced. This way we'll ensure the subscriber count never goes to zero for data + // layer objects that are used by both the old and the new visualization container. - // Bind the visualization objects in the new visualization container to their sources - newVisualizationContainer.UpdateStreamSources(this.DatasetViewModel?.CurrentSessionViewModel); + // Bind the visualization objects in the new visualization container to their sources + newVisualizationContainer.UpdateStreamSources(this.DatasetViewModel?.CurrentSessionViewModel); - // Clear the current visualization container - this.ClearLayout(); + // Clear the current visualization container + this.ClearLayout(); - // Set the new visualization container - this.VisualizationContainer = newVisualizationContainer; + // Set the new visualization container + this.VisualizationContainer = newVisualizationContainer; - // And re-read the stream values at cursor (to publish to stream value visualizers) - DataManager.Instance.ReadAndPublishStreamValue(this.VisualizationContainer.Navigator.Cursor); + // And re-read the stream values at cursor (to publish to stream value visualizers) + DataManager.Instance.ReadAndPublishStreamValue(this.VisualizationContainer.Navigator.Cursor); - return true; + return true; + } } } @@ -152,7 +191,8 @@ public void ClearLayout() /// The type of messages in the stream. public Type GetDataType(string typeName) { - return TypeResolutionHelper.GetVerifiedType(typeName) ?? TypeResolutionHelper.GetVerifiedType(typeName.Split(',')[0]) ?? typeof(object); + return TypeResolutionHelper.GetVerifiedType(typeName) ?? + (this.PluginMap.AdditionalTypeMappings.ContainsKey(typeName) ? this.PluginMap.AdditionalTypeMappings[typeName] : typeof(object)); } /// @@ -228,8 +268,8 @@ public async Task RunSessionBatchProcessingTask(SessionViewModel sessionViewMode public void VisualizeStream(StreamTreeNode streamTreeNode, VisualizerMetadata visualizerMetadata, VisualizationPanel visualizationPanel) { // Create the visualization object - IStreamVisualizationObject visualizationObject = Activator.CreateInstance(visualizerMetadata.VisualizationObjectType) as IStreamVisualizationObject; - visualizationObject.Name = visualizerMetadata.VisualizationFormatString.Replace(VisualizationObjectAttribute.DefaultVisualizationFormatString, streamTreeNode.Path); + var visualizationObject = Activator.CreateInstance(visualizerMetadata.VisualizationObjectType) as IStreamVisualizationObject; + visualizationObject.Name = visualizerMetadata.VisualizationFormatString.Replace(VisualizationObjectAttribute.DefaultVisualizationFormatString, streamTreeNode.FullName); // If the visualization object requires stream supplemental metadata to function, check that we're able to read the supplemental metadata from the stream. if (visualizationObject.RequiresSupplementalMetadata && !streamTreeNode.SupplementalMetadataTypeIsKnown) @@ -260,7 +300,7 @@ public void VisualizeStream(StreamTreeNode streamTreeNode, VisualizerMetadata vi } else { - InstantVisualizationContainer instantVisualizationContainer = Activator.CreateInstance(typeof(InstantVisualizationContainer), visualizationPanel) as InstantVisualizationContainer; + var instantVisualizationContainer = Activator.CreateInstance(typeof(InstantVisualizationContainer), visualizationPanel) as InstantVisualizationContainer; this.VisualizationContainer.AddPanel(instantVisualizationContainer); } } @@ -268,7 +308,7 @@ public void VisualizeStream(StreamTreeNode streamTreeNode, VisualizerMetadata vi // If the target visualization panel is an instant visualization placeholder panel, replace it with a real panel of the correct type else if ((visualizationPanel is InstantVisualizationPlaceholderPanel placeholderPanel) && (visualizationPanel.ParentPanel is InstantVisualizationContainer instantVisualizationContainer)) { - VisualizationPanel replacementPanel = VisualizationPanelFactory.CreateVisualizationPanel(visualizerMetadata.VisualizationPanelType); + var replacementPanel = VisualizationPanelFactory.CreateVisualizationPanel(visualizerMetadata.VisualizationPanelType); instantVisualizationContainer.ReplaceChildVisualizationPanel(placeholderPanel, replacementPanel); visualizationPanel = replacementPanel; } @@ -414,7 +454,10 @@ public void ToggleLiveMode() /// The stream to zoom to. public void ZoomToStreamExtents(StreamTreeNode streamTreeNode) { - this.VisualizationContainer.Navigator.Zoom(streamTreeNode.SubsumedFirstMessageOriginatingTime, streamTreeNode.SubsumedLastMessageOriginatingTime); + if (streamTreeNode.SubsumedFirstMessageOriginatingTime != null && streamTreeNode.SubsumedLastMessageOriginatingTime != null) + { + this.VisualizationContainer.Navigator.Zoom(streamTreeNode.SubsumedFirstMessageOriginatingTime.Value, streamTreeNode.SubsumedLastMessageOriginatingTime.Value); + } } /// diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/AnimatedModel3DVisualizationObject.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/AnimatedModel3DVisualizationObject.cs deleted file mode 100644 index 8e4c10672..000000000 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/AnimatedModel3DVisualizationObject.cs +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. - -namespace Microsoft.Psi.Visualization.VisualizationObjects -{ - using System.Runtime.Serialization; - using MathNet.Spatial.Euclidean; - using Microsoft.Psi.Visualization.Views.Visuals3D; - - /// - /// Implements an animated model 3D visualization object. - /// - [VisualizationObject("Animated Model")] - public class AnimatedModel3DVisualizationObject : XYZValueVisualizationObject - { - private CoordinateSystem cameraTransform; - private string source; - - /// - /// Initializes a new instance of the class. - /// - public AnimatedModel3DVisualizationObject() - { - this.Visual3D = new AnimatedModelVisual(this); - } - - /// - /// Gets or sets the camera transform. - /// - [DataMember] - public CoordinateSystem CameraTransform - { - get { return this.cameraTransform; } - set { this.Set(nameof(this.CameraTransform), ref this.cameraTransform, value); } - } - - /// - /// Gets or sets the source. - /// - [DataMember] - public string Source - { - get { return this.source; } - set { this.Set(nameof(this.Source), ref this.source, value); } - } - } -} diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/Annotations/AnnotationValueEditor.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/Annotations/AnnotationValueEditor.cs index 0c21ce66d..b2cd7136e 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/Annotations/AnnotationValueEditor.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/Annotations/AnnotationValueEditor.cs @@ -4,7 +4,6 @@ namespace Microsoft.Psi.Visualization.VisualizationObjects { using System; - using System.Collections; using System.Linq; using System.Windows; using System.Windows.Controls; diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/Annotations/TimeIntervalAnnotationDisplayData.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/Annotations/TimeIntervalAnnotationDisplayData.cs index 8dd14b1dd..9d11acf7d 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/Annotations/TimeIntervalAnnotationDisplayData.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/Annotations/TimeIntervalAnnotationDisplayData.cs @@ -50,18 +50,18 @@ public class TimeIntervalAnnotationDisplayData : ObservableObject, ICustomTypeDe /// /// Gets the track name for the annotation. /// - public string Track { get; private set; } + public string Track { get; } /// /// Gets the track index for the annotation. /// - public int TrackIndex { get; private set; } + public int TrackIndex { get; } /// /// Gets the annotation schema. /// [Browsable(false)] - public AnnotationSchema AnnotationSchema { get; private set; } + public AnnotationSchema AnnotationSchema { get; } /// /// Gets or sets a value indicating whether the annotation is the currently selected one. diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/Annotations/TimeIntervalAnnotationVisualizationObject.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/Annotations/TimeIntervalAnnotationVisualizationObject.cs index fb9434035..689f0562c 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/Annotations/TimeIntervalAnnotationVisualizationObject.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/Annotations/TimeIntervalAnnotationVisualizationObject.cs @@ -5,6 +5,7 @@ namespace Microsoft.Psi.Visualization.VisualizationObjects { using System; using System.Collections.Generic; + using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; using System.Linq; @@ -24,6 +25,27 @@ namespace Microsoft.Psi.Visualization.VisualizationObjects using Microsoft.Psi.Visualization.VisualizationPanels; using Microsoft.Psi.Visualization.Windows; + /// + /// Defines which tracks to show. + /// + public enum ShowTracks + { + /// + /// Shows all tracks. + /// + All, + + /// + /// Shows all tracks with events in view. + /// + WithEventsInView, + + /// + /// Shows all selected tracks. + /// + Selected, + } + /// /// Implements a visualization object for time interval annotations. /// @@ -48,7 +70,8 @@ public class TimeIntervalAnnotationVisualizationObject : StreamIntervalVisualiza private double lineWidth = 2; private double fontSize = 10; private string legendValue = string.Empty; - private bool showOnlyTracksWithEventsInView = false; + private ShowTracks showTracks = ShowTracks.All; + private ObservableCollection showTracksSelection = null; private RelayCommand mouseLeftButtonDownCommand; private RelayCommand mouseRightButtonDownCommand; @@ -157,15 +180,41 @@ public bool AllowAddOrDeleteAnnotation } /// - /// Gets or sets a value indicating whether to show only tracks with events in view. + /// Gets or sets a value indicating which tracks to show. /// [DataMember] - [DisplayName("Show Only Tracks with Events in View")] - [Description("If true, the visualizer shows only the tracks containing events in view.")] - public bool ShowOnlyTracksWithEventsInView + [DisplayName("Show Tracks")] + [Description("Specifies which tracks to show.")] + public ShowTracks ShowTracks { - get { return this.showOnlyTracksWithEventsInView; } - set { this.Set(nameof(this.ShowOnlyTracksWithEventsInView), ref this.showOnlyTracksWithEventsInView, value); } + get { return this.showTracks; } + set { this.Set(nameof(this.ShowTracks), ref this.showTracks, value); } + } + + /// + /// Gets or sets the set of tracks to display when Show Tracks is set to Selected. + /// + [DataMember] + [DisplayName("Show Tracks Selection")] + [Description("Set of tracks to display when Show Tracks is set to Selected.")] + public ObservableCollection ShowTracksSelection + { + get { return this.showTracksSelection; } + + set + { + if (this.showTracksSelection != null) + { + this.showTracksSelection.CollectionChanged -= this.OnSelectedTracksCollectionChanged; + } + + this.Set(nameof(this.showTracksSelection), ref this.showTracksSelection, value); + + if (this.showTracksSelection != null) + { + this.showTracksSelection.CollectionChanged += this.OnSelectedTracksCollectionChanged; + } + } } /// @@ -186,6 +235,13 @@ public bool ShowOnlyTracksWithEventsInView [IgnoreDataMember] public List DisplayData { get; private set; } = new List(); + /// + /// Gets the current set of tracks. + /// + [Browsable(false)] + [IgnoreDataMember] + public IEnumerable Tracks => this.trackIndex.Keys; + /// /// Gets the current number of tracks. /// @@ -260,6 +316,91 @@ public RelayCommand MouseDoubleClickCommand [IgnoreDataMember] public TimelineVisualizationPanel TimelineVisualizationPanel => this.Panel as TimelineVisualizationPanel; + /// + public override List ContextMenuItemsInfo() + { + var items = new List(); + + // Get the track under the cursor + var trackUnderCursor = this.GetTrackByIndex(this.TrackUnderMouseIndex); + + // Add the add annotation (on current track) context menu item + var newAnnotationOnCurrentTrackCommand = this.GetCreateAnnotationOnTrackCommand(trackUnderCursor); + items.Add( + new ContextMenuItemInfo( + IconSourcePath.Annotation, + "New Annotation", + newAnnotationOnCurrentTrackCommand, + isEnabled: newAnnotationOnCurrentTrackCommand != null)); + + // Add the add annotation on new track context menu item + var newAnnotationOnNewTrackCommand = this.GetNewAnnotationOnNewTrackCommand(); + items.Add( + new ContextMenuItemInfo( + null, + "New Annotation on New Track", + newAnnotationOnNewTrackCommand, + isEnabled: newAnnotationOnNewTrackCommand != null)); + + // Add the set annotation time interval to selection boundaries menu item + var setAnnotationTimeIntervalToSelectionCommand = this.GetSetAnnotationTimeIntervalToSelectionCommand(this.Navigator.Cursor, trackUnderCursor); + items.Add( + new ContextMenuItemInfo( + IconSourcePath.SetAnnotationToSelection, + "Adjust Annotation to Selection Boundaries", + setAnnotationTimeIntervalToSelectionCommand, + isEnabled: setAnnotationTimeIntervalToSelectionCommand != null)); + + // Add the delete annotation context menu item + var deleteCommand = this.GetDeleteAnnotationOnTrackCommand(this.Navigator.Cursor, trackUnderCursor); + items.Add( + new ContextMenuItemInfo( + null, + "Delete Annotation", + deleteCommand, + isEnabled: deleteCommand != null)); + + // Add the delete annotation context menu item + var deleteAllAnnotationsOnTrackCommand = this.GetDeleteAllAnnotationsOnTrackCommand(trackUnderCursor); + items.Add( + new ContextMenuItemInfo( + null, + $"Delete All Annotations on Curent Track ({trackUnderCursor})", + deleteAllAnnotationsOnTrackCommand, + isEnabled: deleteAllAnnotationsOnTrackCommand != null)); + + // Add the delete annotation context menu item + var renameCurrentTrackCommand = this.GetRenameTrackCommand(trackUnderCursor); + items.Add( + new ContextMenuItemInfo( + null, + $"Rename Curent Track ({trackUnderCursor})", + renameCurrentTrackCommand, + isEnabled: renameCurrentTrackCommand != null)); + + // Add the command to show all tracks + var showAllTracksCommand = this.GetShowAllTracksCommand(); + if (showAllTracksCommand != null) + { + items.Add(new ContextMenuItemInfo(null, $"Show All Tracks", showAllTracksCommand)); + } + + // Add the command to show only tracks with events in view + var showOnlyTracksWithEventsInViewCommand = this.GetShowOnlyTracksWithEventsInViewCommand(); + if (showOnlyTracksWithEventsInViewCommand != null) + { + items.Add(new ContextMenuItemInfo(null, $"Show Only Tracks with Events in View", showOnlyTracksWithEventsInViewCommand)); + } + + // Add a separator + items.Add(null); + + // Add the base visualization object commands + items.AddRange(base.ContextMenuItemsInfo()); + + return items; + } + /// /// Update the specified annotation set message. /// @@ -274,11 +415,11 @@ public void UpdateAnnotationSetMessage(Message annota } /// - /// Gets the command for adding an annotation to the specified track. + /// Gets the command for adding a new annotation on a specified track. /// - /// The track name. - /// The command for adding an annotation to the specified track. - public ICommand GetAddAnnotationOnTrackCommand(string track) + /// The track to add the annotation on. + /// The command for adding a new annotation to the specified track. + public ICommand GetCreateAnnotationOnTrackCommand(string track) { if (track == null) { @@ -310,19 +451,62 @@ public ICommand GetAddAnnotationOnTrackCommand(string track) return this.CreateEditAnnotationErrorCommand(ErrorSelectionMarkersUnset); } - if (this.TrackHasAnnotationsOverlappingWith(track, selectionTimeInterval)) + if (this.Data != null && this.Data.Select(m => m.Data).GetAnnotationTimeIntervalOverlappingWith(track, selectionTimeInterval, out var _)) + { + return this.CreateEditAnnotationErrorCommand(ErrorOverlappingAnnotations); + } + + return new PsiCommand(() => this.CreateAnnotation(selectionTimeInterval, track)); + } + + /// + /// Gets the command for adding a specified annotation. + /// + /// The annotation to add. + /// The command for adding an annotation to the specified track. + public ICommand GetAddAnnotationCommand(TimeIntervalAnnotation annotation) + { + if (annotation == null) + { + return null; + } + + // All of the following must be true to allow an annotation to be added: + // + // 1) We must be bound to a source + // 2) Add/Delete annotations must be enabled. + // 3) Both selection markers must be set. + // 4) There must be no annotations between the selection markers. + // + // If one of these conditions does not hold, display an error message + if (!this.IsBound) + { + return this.CreateEditAnnotationErrorCommand(ErrorStreamNotBound); + } + + if (!this.AllowAddOrDeleteAnnotation) + { + return this.CreateEditAnnotationErrorCommand(ErrorEditingDisabled); + } + + if ((annotation.Interval.Left <= DateTime.MinValue) || (annotation.Interval.Right >= DateTime.MaxValue)) + { + return this.CreateEditAnnotationErrorCommand(ErrorSelectionMarkersUnset); + } + + if (this.Data != null && this.Data.Select(m => m.Data).GetAnnotationTimeIntervalOverlappingWith(annotation.Track, annotation.Interval, out var _)) { return this.CreateEditAnnotationErrorCommand(ErrorOverlappingAnnotations); } - return new PsiCommand(() => this.AddAnnotation(selectionTimeInterval, track)); + return new PsiCommand(() => this.AddAnnotation(annotation)); } /// /// Gets the command for adding an annotation to a new track. /// /// The command for adding an annotation to a new track. - public ICommand GetAddAnnotationOnNewTrackCommand() + public ICommand GetNewAnnotationOnNewTrackCommand() { // All of the following must be true to allow an annotation to be added: // @@ -348,15 +532,69 @@ public ICommand GetAddAnnotationOnNewTrackCommand() return this.CreateEditAnnotationErrorCommand(ErrorSelectionMarkersUnset); } - return new PsiCommand(() => this.AddAnnotation(selectionTimeInterval, null)); + return new PsiCommand(() => this.CreateAnnotation(selectionTimeInterval, null)); + } + + /// + /// Gets the command for setting the annotation time interval to the selection boundaries. + /// + /// The cursor time. + /// The track name. + /// The command for setting the annotation time interval to the selection boundaries. + public ICommand GetSetAnnotationTimeIntervalToSelectionCommand(DateTime cursorTime, string track) +{ + if (this.Navigator.SelectionRange.StartTime == DateTime.MinValue || this.Navigator.SelectionRange.EndTime == DateTime.MaxValue) + { + return null; + } + + // All of the following must be true to edit the annotation boundaries: + // + // 1) We must be bound to a source + // 2) EditAnnotationBoundaries annotations must be enabled. + // 3) The mouse cursor must be above an existing annotation. + // 4) There must be no other annotation on the specified track between + // the selection markers. + var selectionTimeInterval = this.Container.Navigator.SelectionRange.AsTimeInterval; + + if (!this.IsBound) + { + return this.CreateEditAnnotationErrorCommand(ErrorStreamNotBound); + } + + if (!this.AllowEditAnnotationBoundaries) + { + return this.CreateEditAnnotationErrorCommand(ErrorEditingDisabled); + } + + // Get the annotation under the cursor on the specified track + var annotation = this.Data?.Select(m => m.Data).GetTimeIntervalAnnotationAtTime(cursorTime, track); + + if (annotation == null) + { + return null; + } + + // Check if there is a different annotation overlapping on the same track + if (this.Data != null && this.Data.Select(m => m.Data).GetAnnotationTimeIntervalOverlappingWith(track, selectionTimeInterval, out var intersectingTimeInterval)) + { + if ((intersectingTimeInterval.Left != annotation.Interval.Left) || + (intersectingTimeInterval.Right != annotation.Interval.Right)) + { + return this.CreateEditAnnotationErrorCommand(ErrorOverlappingAnnotations); + } + } + + return new PsiCommand(() => this.EditAnnotationTimeInterval(annotation, this.Navigator.SelectionRange.AsTimeInterval)); } /// /// Gets the command for deleting the annotation on a specified track. /// + /// The cursor time. /// The track name. /// The command for deleting the annotation on a specified track. - public ICommand GetDeleteAnnotationOnTrackCommand(string track) + public ICommand GetDeleteAnnotationOnTrackCommand(DateTime cursorTime, string track) { if (track == null) { @@ -381,8 +619,8 @@ public ICommand GetDeleteAnnotationOnTrackCommand(string track) } // Get the index of the annotation under the cursor on the specified track - var index = this.GetAnnotationIndex(this.Container.Navigator.Cursor, track); - return (index >= 0) ? new PsiCommand(() => this.DeleteAnnotation(index, track)) : null; + var annotation = this.Data?.Select(m => m.Data).GetTimeIntervalAnnotationAtTime(cursorTime, track); + return (annotation != null) ? new PsiCommand(() => this.DeleteAnnotation(annotation)) : null; } /// @@ -415,14 +653,14 @@ public ICommand GetRenameTrackCommand(string track) /// /// The command to show all tracks. public ICommand GetShowAllTracksCommand() - => this.ShowOnlyTracksWithEventsInView ? new PsiCommand(() => this.ShowOnlyTracksWithEventsInView = false) : null; + => this.ShowTracks != ShowTracks.All ? new PsiCommand(() => this.ShowTracks = ShowTracks.All) : null; /// /// Gets the command to show only tracks with events in view. /// /// The command to show tracks with events in view. public ICommand GetShowOnlyTracksWithEventsInViewCommand() - => !this.ShowOnlyTracksWithEventsInView ? new PsiCommand(() => this.ShowOnlyTracksWithEventsInView = true) : null; + => this.ShowTracks != ShowTracks.WithEventsInView ? new PsiCommand(() => this.ShowTracks = ShowTracks.WithEventsInView) : null; /// /// Gets the name of a track with the specified index. @@ -445,6 +683,7 @@ protected override void OnStreamBound() throw new Exception("Cannot find annotation schema."); } + this.UpdateDisplayData(); this.GenerateLegendValue(); } @@ -467,7 +706,7 @@ protected override void OnPropertyChanged(object sender, PropertyChangedEventArg { base.OnPropertyChanged(sender, e); - if (e.PropertyName == nameof(this.ShowOnlyTracksWithEventsInView)) + if (e.PropertyName == nameof(this.ShowTracks)) { this.UpdateDisplayData(); } @@ -478,7 +717,7 @@ protected override void OnViewRangeChanged(object sender, Navigation.NavigatorTi { base.OnViewRangeChanged(sender, e); - if (this.ShowOnlyTracksWithEventsInView) + if (this.ShowTracks == ShowTracks.WithEventsInView) { this.UpdateDisplayData(); } @@ -488,18 +727,14 @@ private PsiCommand CreateEditAnnotationErrorCommand(string errorMessage) => new (() => new MessageBoxWindow(Application.Current.MainWindow, "Error Editing Annotation", errorMessage, "Close", null).ShowDialog()); /// - /// Adds a new annotation on a specified track. + /// Creates a new annotation on a specified track. /// /// The time interval for the annotation. /// The track for the annotation. If null, a new track is generated. - private void AddAnnotation(TimeInterval timeInterval, string track) + private void CreateAnnotation(TimeInterval timeInterval, string track) { - // If the track name is not specified - if (track == null) - { - // Then create a new track - track = this.CreateNewTrack(); - } + // Create a new track if one is not specified + track ??= this.CreateNewTrack(); // Create the annotation using the annotation schema var annotation = this.AnnotationSchema.CreateDefaultTimeIntervalAnnotation(timeInterval, track); @@ -529,14 +764,43 @@ private void AddAnnotation(TimeInterval timeInterval, string track) } /// - /// Deletes an existing annotation, specified by a data index and a track name. + /// Adds a specified annotation. /// - /// The data index. - /// The track name. - private void DeleteAnnotation(int index, string track) + /// The annotation to add. + private void AddAnnotation(TimeIntervalAnnotation annotation) + { + // Now find if we need to alter an existing annotation set, or create a new one + var existingAnnotationSetMessage = this.Data.FirstOrDefault(m => m.OriginatingTime == annotation.Interval.Right); + if (existingAnnotationSetMessage != default) + { + existingAnnotationSetMessage.Data.AddAnnotation(annotation); + var streamUpdate = new StreamUpdate[] { new (StreamUpdateType.Replace, existingAnnotationSetMessage) }; + DataManager.Instance.UpdateStream(this.StreamSource, streamUpdate); + } + else + { + // Create a message for the annotation + var annotationSet = new TimeIntervalAnnotationSet(annotation); + var newAnnotationSetMessage = new Message(annotationSet, annotation.Interval.Right, annotation.Interval.Right, 0, 0); + var streamUpdate = new StreamUpdate[] { new (StreamUpdateType.Add, newAnnotationSetMessage) }; + DataManager.Instance.UpdateStream(this.StreamSource, streamUpdate); + } + + // Update the data + this.UpdateDisplayData(); + + // Display the properties of the new annotation + this.SelectAnnotation(annotation); + } + + /// + /// Deletes an existing annotation. + /// + /// The annotation to delete. + private void DeleteAnnotation(TimeIntervalAnnotation annotation) { - var annotationSetMessage = this.Data[index]; - var annotation = annotationSetMessage.Data[track]; + // Find the corresponding annotation set message + var annotationSetMessage = this.FindTimeIntervalAnnotationSetMessageContaining(annotation); // If the annotation is currently selected, then deselect it if (this.selectedDisplayDataItem != null && this.selectedDisplayDataItem.Annotation == annotation) @@ -556,7 +820,7 @@ private void DeleteAnnotation(int index, string track) else { // otherwise remove the annotation from the annotation set - annotationSetMessage.Data.RemoveAnnotation(track); + annotationSetMessage.Data.RemoveAnnotation(annotation.Track); // and update the annotation set updates.Add(new StreamUpdate(StreamUpdateType.Replace, annotationSetMessage)); @@ -569,6 +833,38 @@ private void DeleteAnnotation(int index, string track) this.UpdateDisplayData(); } + /// + /// Edits the boundaries of an existing annotation. + /// + /// The annotation to edit. + /// The new time interval for the annotation. + private void EditAnnotationTimeInterval(TimeIntervalAnnotation annotation, TimeInterval timeInterval) + { + // Find the corresponding annotation set message + var annotationSetMessage = this.FindTimeIntervalAnnotationSetMessageContaining(annotation); + + // If the annotation is currently selected, then deselect it + if (this.selectedDisplayDataItem != null && this.selectedDisplayDataItem.Annotation == annotation) + { + this.SelectAnnotation(null); + } + + // Create the list of stream updates + var updates = new List>(); + + // Set the time interval + annotation.Interval = timeInterval.DeepClone(); + + // and update the annotation set + updates.Add(new StreamUpdate(StreamUpdateType.Replace, annotationSetMessage)); + + // Update the stream + DataManager.Instance.UpdateStream(this.StreamSource, updates); + + // Update the view + this.UpdateDisplayData(); + } + /// /// Delete all annotations on a specified track. /// @@ -655,81 +951,55 @@ private void RenameTrack(string track) } } - /// - /// Determines whether any annotation on the specified track intersects with the specified time interval. - /// - /// The track name. - /// The time interval. - /// True if any annotation on the specified track intersects with the specified time interval, otherwise false. - private bool TrackHasAnnotationsOverlappingWith(string track, TimeInterval timeInterval) - { - // Start by building up a list of indices where the annotation set has an annotation on - // the specified track. (We will need to search among the annotations for these indices) - var indexList = this.GetDataIndexListForTrack(track); - - // If there's no annotations at all, we're done and there's no intersection. - if (indexList.Count == 0) - { - return false; - } - - // Find the nearest annotation to the left edge of the interval - int index = IndexHelper.GetIndexForTime(timeInterval.Left, indexList.Count, i => this.Data[indexList[i]].Data[track].Interval.Right, NearestMessageType.Nearest); - - // Check if the annotation intersects with the interval, then keep walking to the right until - // we find an annotation within the interval or we go past the right hand side of the interval. - while (index < indexList.Count) - { - var annotation = this.Data[indexList[index]].Data[track]; - - // Check if the annotation is completely to the left of the interval - // NOTE: By default time intervals are inclusive of their endpoints, so abutting time intervals will - // test as intersecting. Use a non-inclusive time interval so that we can let annotations abut. - if (timeInterval.IntersectsWith(new TimeInterval(annotation.Interval.Left, false, annotation.Interval.Right, false))) - { - return true; - } - - // Check if the annotation is completely to the right of the interval - if (timeInterval.Right <= annotation.Interval.Left) - { - return false; - } - - index++; - } - - return false; - } - private void UpdateDisplayData() { // Rebuild the data this.RaisePropertyChanging(nameof(this.DisplayData)); this.DisplayData.Clear(); - this.trackIndex.Clear(); - // Compute the tracks - var viewInterval = default(TimeInterval); if (this.IsBound && this.Data != null) { - // Compute the view interval - viewInterval = new TimeInterval(this.Navigator.ViewRange.StartTime, this.Navigator.ViewRange.EndTime); + // Get the view interval + var viewInterval = new TimeInterval(this.Navigator.ViewRange.StartTime, this.Navigator.ViewRange.EndTime); - // Go through the messages and update the track index + // Then recompute the track index + this.trackIndex.Clear(); + + // Go through the messages and figure out all the tracks and which ones have events in view + var tracks = new Dictionary(); foreach (var annotationsMessage in this.Data) { foreach (var track in annotationsMessage.Data.Tracks) { - if (!this.ShowOnlyTracksWithEventsInView || annotationsMessage.Data[track].Interval.IntersectsWith(viewInterval)) + var eventInView = annotationsMessage.Data[track].Interval.IntersectsWith(viewInterval); + if (!tracks.ContainsKey(track)) + { + tracks.Add(track, eventInView); + } + else if (eventInView) { - if (!this.trackIndex.ContainsKey(track)) - { - this.trackIndex.Add(track, this.trackIndex.Count); - } + tracks[track] = true; } } } + + // If we are showing tracks with events in view + if (this.ShowTracks == ShowTracks.WithEventsInView) + { + // Sort the tracks that have events in view alphabetically + this.ShowTracksSelection = new ObservableCollection(tracks.Keys.Where(t => tracks[t]).OrderBy(x => x).ToList()); + } + else if (this.ShowTracks == ShowTracks.All) + { + // O/w if we are showing all tracks sort the tracks alphabetically + this.ShowTracksSelection = new ObservableCollection(tracks.Keys.OrderBy(x => x).ToList()); + } + + // Now build the tracks index from the selected tracks + foreach (var track in this.ShowTracksSelection) + { + this.trackIndex.Add(track, this.trackIndex.Count); + } } // If no tracks exist, initialize a new track @@ -739,14 +1009,19 @@ private void UpdateDisplayData() } // Now compute the display data - if (this.IsBound && this.Data != null) + if (this.IsBound && this.Data != null && this.AnnotationSchema != null) { + // Get the view interval + var viewInterval = new TimeInterval(this.Navigator.ViewRange.StartTime, this.Navigator.ViewRange.EndTime); + // Finally, reconstruct the items to be displayed foreach (var annotationsMessage in this.Data) { foreach (var track in annotationsMessage.Data.Tracks) { - if (!this.ShowOnlyTracksWithEventsInView || annotationsMessage.Data[track].Interval.IntersectsWith(viewInterval)) + if (this.ShowTracks == ShowTracks.All || + (this.ShowTracks == ShowTracks.WithEventsInView && annotationsMessage.Data[track].Interval.IntersectsWith(viewInterval)) || + (this.ShowTracks == ShowTracks.Selected && this.trackIndex.ContainsKey(track))) { this.DisplayData.Add(new TimeIntervalAnnotationDisplayData(this, annotationsMessage, track, this.trackIndex[track], this.AnnotationSchema)); } @@ -768,19 +1043,18 @@ private void DoMouseDoubleClick(MouseButtonEventArgs e) // Get the time at the mouse cursor var cursorTime = (this.Panel as TimelineVisualizationPanel).GetTimeAtMousePointer(e, false); - // Get the item (if any) that straddles this time - int index = this.GetAnnotationIndex(cursorTime, trackUnderMouse); - if (index > -1) + // Get the annotation (if any) that straddles this time + var annotation = this.Data?.Select(m => m.Data).GetTimeIntervalAnnotationAtTime(cursorTime, trackUnderMouse); + if (annotation != null) { // Set the navigator selection to the bounds of the annotation - var annotationTimeInterval = this.Data[index].Data[trackUnderMouse].Interval; - this.Navigator.SelectionRange.Set(annotationTimeInterval); + this.Navigator.SelectionRange.Set(annotation.Interval); // If the shift key was down, then also zoom to the annotation (with 10% empty space to the left and right) if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift)) { - double bufferSeconds = annotationTimeInterval.Span.TotalSeconds * 0.1d; - this.Navigator.ViewRange.Set(annotationTimeInterval.Left.AddSeconds(-bufferSeconds), annotationTimeInterval.Right.AddSeconds(bufferSeconds)); + double bufferSeconds = annotation.Interval.Span.TotalSeconds * 0.1d; + this.Navigator.ViewRange.Set(annotation.Interval.Left.AddSeconds(-bufferSeconds), annotation.Interval.Right.AddSeconds(bufferSeconds)); } } } @@ -798,19 +1072,19 @@ private void DoMouseLeftButtonDown(MouseButtonEventArgs e) var timelineScroller = this.TimelineVisualizationPanel.GetTimelineScroller(e.Source); var annotationSetMessage = default(Message); - var annotation = default(TimeIntervalAnnotation); + var annotationSetMessageIndex = -1; var annotationEdge = AnnotationEdge.None; - // Get the item (if any) that straddles this time - int index = this.GetAnnotationIndex(cursorTime, trackUnderMouse); + // Get the annotation (if any) that straddles this time + var annotation = this.Data?.Select(m => m.Data).GetTimeIntervalAnnotationAtTime(cursorTime, trackUnderMouse); - if (index > -1 && trackUnderMouse != null) + if (annotation != null && trackUnderMouse != null) { - // Get the annotation set - annotationSetMessage = this.Data[index]; + // Set the corresponding annotation set message + annotationSetMessage = this.FindTimeIntervalAnnotationSetMessageContaining(annotation); - // Get the annotation that was hit - annotation = annotationSetMessage.Data[trackUnderMouse]; + // Set the corresponding annotation set message index + annotationSetMessageIndex = this.Data.IndexOf(annotationSetMessage); // Check if the mouse is over an edge of the annotation annotationEdge = this.GetMouseOverAnnotationEdge(cursorTime, annotation, timelineScroller); @@ -861,12 +1135,12 @@ private void DoMouseLeftButtonDown(MouseButtonEventArgs e) { // Get the previous and next annotation sets containing an annotation for the current track (if any) // and check if they abut the annotation whose edge we're going to drag - Message previousAnnotationSet = this.Data.Take(index).LastOrDefault(a => a.Data.ContainsTrack(annotation.Track)); + Message previousAnnotationSet = this.Data.Take(annotationSetMessageIndex).LastOrDefault(a => a.Data.ContainsTrack(annotation.Track)); Message? previousAnnotationSetNullable = previousAnnotationSet != default ? previousAnnotationSet : default; bool previousAnnotationSetFound = previousAnnotationSet != default; bool previousAnnotationAbuts = previousAnnotationSetFound && previousAnnotationSet.Data[annotation.Track].Interval.Right == annotation.Interval.Left; - Message nextAnnotationSet = this.Data.Skip(index + 1).FirstOrDefault(a => a.Data.ContainsTrack(annotation.Track)); + Message nextAnnotationSet = this.Data.Skip(annotationSetMessageIndex + 1).FirstOrDefault(a => a.Data.ContainsTrack(annotation.Track)); Message? nextAnnotationSetNullable = nextAnnotationSet != default ? nextAnnotationSet : default; bool nextAnnotationSetFound = nextAnnotationSet != default; bool nextAnnotationAbuts = nextAnnotationSetFound && nextAnnotationSet.Data[annotation.Track].Interval.Left == annotation.Interval.Right; @@ -951,16 +1225,12 @@ private void DoMouseRightButtonDown(MouseButtonEventArgs e) // Get the time at the mouse cursor DateTime cursorTime = this.TimelineVisualizationPanel.GetTimeAtMousePointer(e, false); - var annotation = default(TimeIntervalAnnotation); var annotationEdge = AnnotationEdge.None; - // Get the item (if any) that straddles this time - int index = this.GetAnnotationIndex(cursorTime, trackUnderMouse); - if (index > -1 && trackUnderMouse != null) + // Get the annotation (if any) that straddles this time + var annotation = this.Data?.Select(m => m.Data).GetTimeIntervalAnnotationAtTime(cursorTime, trackUnderMouse); + if (annotation != null && trackUnderMouse != null) { - // Get the annotation that was hit - annotation = this.Data[index].Data[trackUnderMouse]; - // Check if the mouse is over an edge of the annotation annotationEdge = this.GetMouseOverAnnotationEdge(cursorTime, annotation, this.TimelineVisualizationPanel.GetTimelineScroller(e.Source)); } @@ -999,12 +1269,9 @@ private void DoMouseMove(MouseEventArgs e) if (this.AllowEditAnnotationBoundaries) { // Get the item (if any) that straddles this time - int index = this.GetAnnotationIndex(timeAtMousePointer, trackUnderMouse); - if (index > -1 && trackUnderMouse != null) + var annotation = this.Data?.Select(m => m.Data).GetTimeIntervalAnnotationAtTime(timeAtMousePointer, trackUnderMouse); + if (annotation != null && trackUnderMouse != null) { - // Get the annotation that was hit - var annotation = this.Data[index].Data[trackUnderMouse]; - // Check if the mouse is over an edge of the annotation var annotationEdge = this.GetMouseOverAnnotationEdge(timeAtMousePointer, annotation, this.TimelineVisualizationPanel.GetTimelineScroller(e.Source)); @@ -1142,74 +1409,6 @@ private void UpdateAnnotationSetMessageTime(Message a DataManager.Instance.UpdateStream(this.StreamSource, updates); } - private int GetAnnotationIndex(DateTime time, string track) - { - if (track == null) - { - return -1; - } - - var indexList = this.GetDataIndexListForTrack(track); - - if (indexList.Count > 0) - { - var index = this.GetTimeIntervalItemIndexByTime( - time, - indexList.Count, - i => this.Data[indexList[i]].Data[track].Interval.Left, - i => this.Data[indexList[i]].Data[track].Interval.Right); - return index == -1 ? -1 : indexList[index]; - } - else - { - return -1; - } - } - - private int GetTimeIntervalItemIndexByTime(DateTime time, int count, Func startTimeAtIndex, Func endTimeAtIndex) - { - if (count < 1) - { - return -1; - } - - int lo = 0; - int hi = count - 1; - while ((lo != hi - 1) && (lo != hi)) - { - var val = (lo + hi) / 2; - if (endTimeAtIndex(val) < time) - { - lo = val; - } - else if (startTimeAtIndex(val) > time) - { - hi = val; - } - else - { - return val; - } - } - - // If lo and hi differ by 1, then either of those value could be straddled by the first or last - // annotation. If lo and hi are both 0 then there's only 1 element so we should test it as well. - if (hi - lo <= 1) - { - if ((endTimeAtIndex(hi) >= time) && (startTimeAtIndex(hi) <= time)) - { - return hi; - } - - if ((endTimeAtIndex(lo) >= time) && (startTimeAtIndex(lo) <= time)) - { - return lo; - } - } - - return -1; - } - private AnnotationEdge GetMouseOverAnnotationEdge(DateTime cursorTime, TimeIntervalAnnotation annotation, TimelineScroller timelineScroller) { // Work out what time interval is expressed in 3 pixels at the current zoom @@ -1299,29 +1498,6 @@ private void GenerateLegendValue() this.RaisePropertyChanged(nameof(this.AttributeCount)); } - /// - /// Gets the list of indices for annotation set messages that contain an annotation on the specified track. - /// - /// The track name. - /// The list of indices for annotation set messages that contain an annotation on the specified track. - private List GetDataIndexListForTrack(string track) - { - var indexList = new List(); - - if (this.Data != null) - { - for (int i = 0; i < this.Data.Count; i++) - { - if (this.Data[i].Data.ContainsTrack(track)) - { - indexList.Add(i); - } - } - } - - return indexList; - } - private string CreateNewTrack() { while (this.trackIndex.ContainsKey($"Track:{this.newTrackId}")) @@ -1334,5 +1510,17 @@ private string CreateNewTrack() return track; } + + private Message FindTimeIntervalAnnotationSetMessageContaining(TimeIntervalAnnotation annotation) + => this.Data.First(asm => asm.Data.ContainsTrack(annotation.Track) && asm.Data[annotation.Track] == annotation); + + private void OnSelectedTracksCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + // If the selected tracks collection has changed, switch the show tracks mode to selected + this.ShowTracks = ShowTracks.Selected; + + // An update the data + this.UpdateDisplayData(); + } } } \ No newline at end of file diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/AudioVisualizationObject.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/AudioVisualizationObject.cs index 13398684f..d3867467a 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/AudioVisualizationObject.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/AudioVisualizationObject.cs @@ -3,11 +3,13 @@ namespace Microsoft.Psi.Visualization.VisualizationObjects { + using System.Collections.Generic; using System.ComponentModel; using System.Runtime.Serialization; using System.Windows; using GalaSoft.MvvmLight.CommandWpf; using Microsoft.Psi.Visualization; + using Microsoft.Psi.Visualization.Data; using Microsoft.Psi.Visualization.Helpers; using Microsoft.Psi.Visualization.Summarizers; using Microsoft.Psi.Visualization.Views.Visuals2D; @@ -85,7 +87,17 @@ public short Channel // NOTE: Only open a stream when this visualization object is connected to it's parent // Create a new binding with a different channel argument and re-open the stream - this.StreamBinding.SummarizerArguments = new object[] { this.Channel }; + this.StreamBinding = new StreamBinding( + this.StreamBinding.SourceStreamName, + this.StreamBinding.PartitionName, + this.StreamBinding.StreamName, + this.StreamBinding.DerivedStreamAdapterType, + this.StreamBinding.DerivedStreamAdapterArguments, + this.StreamBinding.VisualizerStreamAdapterType, + this.StreamBinding.VisualizerStreamAdapterArguments, + this.StreamBinding.VisualizerSummarizerType, + new object[] { this.Channel }); + this.OnStreamBound(); } } @@ -133,6 +145,18 @@ public override string IconSource [IgnoreDataMember] public string EnableAudioCommandText => this.Navigator.IsAudioPlaybackVisualizationObject(this) ? $"Mute {this.Name}" : $"Enable {this.Name}"; + /// + public override List ContextMenuItemsInfo() + { + var items = new List() + { + new ContextMenuItemInfo(this.ContextMenuIconSource, this.EnableAudioCommandText, this.EnableAudioCommand), + }; + + items.AddRange(base.ContextMenuItemsInfo()); + return items; + } + /// public override double GetNumericValue(double data) { diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/CoordinateSystemVisualizationObject.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/CoordinateSystemVisualizationObject.cs index d2c9b839e..45e194861 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/CoordinateSystemVisualizationObject.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/CoordinateSystemVisualizationObject.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -#pragma warning disable SA1305 // Variable should not use Hungarian notation - namespace Microsoft.Psi.Visualization.VisualizationObjects { using System.ComponentModel; diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/DepthImageVisualizationObject.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/DepthImageVisualizationObject.cs index 8986cdffb..9953c3715 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/DepthImageVisualizationObject.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/DepthImageVisualizationObject.cs @@ -3,9 +3,11 @@ namespace Microsoft.Psi.Visualization.VisualizationObjects { + using System.Collections.Generic; using System.ComponentModel; using System.Runtime.Serialization; using System.Windows; + using GalaSoft.MvvmLight.CommandWpf; using Microsoft.Psi.Imaging; using Microsoft.Psi.Visualization.Helpers; using Microsoft.Psi.Visualization.Views.Visuals2D; @@ -221,6 +223,31 @@ public static (int Min, int Max, int Invalid) GetRange(DepthImageRangeMode depth } } + /// + public override List ContextMenuItemsInfo() + { + var items = base.ContextMenuItemsInfo(); + + // Add Set Range mode commands + var rangeModeItems = new ContextMenuItemInfo("Set Range Mode"); + + rangeModeItems.SubItems.Add( + new ContextMenuItemInfo( + string.Empty, + DepthImageRangeMode.Auto.ToString(), + new RelayCommand(() => this.RangeMode = DepthImageRangeMode.Auto))); + + rangeModeItems.SubItems.Add( + new ContextMenuItemInfo( + string.Empty, + DepthImageRangeMode.Maximum.ToString(), + new RelayCommand(() => this.RangeMode = DepthImageRangeMode.Maximum))); + + items.Add(rangeModeItems); + + return items; + } + /// protected override void OnPropertyChanged(object sender, PropertyChangedEventArgs e) { diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/ModelVisual3DCollectionVisualizationObject.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/ModelVisual3DCollectionVisualizationObject.cs index f93166003..5ab514c4c 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/ModelVisual3DCollectionVisualizationObject.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/ModelVisual3DCollectionVisualizationObject.cs @@ -5,7 +5,6 @@ namespace Microsoft.Psi.Visualization.VisualizationObjects { using System.Collections.Generic; using System.ComponentModel; - using System.Reflection; using System.Runtime.Serialization; /// diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/ModelVisual3DVisualizationObject.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/ModelVisual3DVisualizationObject.cs index f83f1b9ab..669e26446 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/ModelVisual3DVisualizationObject.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/ModelVisual3DVisualizationObject.cs @@ -25,9 +25,9 @@ public abstract class ModelVisual3DVisualizationObject : XYZValueVisualiz nameof(CursorEpsilonPosMs), nameof(Name), nameof(PartitionName), - nameof(StreamName), - nameof(StreamAdapterDisplayName), - nameof(SummarizerTypeDisplayName), + nameof(SourceStreamName), + nameof(StreamAdapterTypeDisplayString), + nameof(SummarizerTypeDisplayString), }; // The time elapsed while updating the data diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/NumericPlot/PlotSeriesVisualizationObject.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/NumericPlot/PlotSeriesVisualizationObject.cs index 05ad3e161..0adbe0d72 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/NumericPlot/PlotSeriesVisualizationObject.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/NumericPlot/PlotSeriesVisualizationObject.cs @@ -195,9 +195,26 @@ protected set public abstract string GetStringValue(TData data); /// - protected override int GetIndexForTime(DateTime currentTime, int count, Func timeAtIndex) + public override List ContextMenuItemsInfo() { - return IndexHelper.GetIndexForTime(currentTime, count, timeAtIndex); + var items = base.ContextMenuItemsInfo(); + + if (this.MarkerStyle == MarkerStyle.None) + { + items.Add(new ContextMenuItemInfo( + null, + "Show Markers", + new VisualizationCommand(() => this.MarkerStyle = MarkerStyle.Circle))); + } + else + { + items.Add(new ContextMenuItemInfo( + null, + "Hide Markers", + new VisualizationCommand(() => this.MarkerStyle = MarkerStyle.None))); + } + + return items; } /// diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/NumericPlot/PlotVisualizationObject{TData}.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/NumericPlot/PlotVisualizationObject{TData}.cs index 205c426e0..54e906a7c 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/NumericPlot/PlotVisualizationObject{TData}.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/NumericPlot/PlotVisualizationObject{TData}.cs @@ -6,12 +6,12 @@ namespace Microsoft.Psi.Visualization.VisualizationObjects { using System; + using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; using System.Runtime.Serialization; using System.Windows.Media; using Microsoft.Psi.Visualization; - using Microsoft.Psi.Visualization.Helpers; using Microsoft.Psi.Visualization.VisualizationPanels; /// @@ -228,9 +228,26 @@ protected set public abstract string GetStringValue(TData data); /// - protected override int GetIndexForTime(DateTime currentTime, int count, Func timeAtIndex) + public override List ContextMenuItemsInfo() { - return IndexHelper.GetIndexForTime(currentTime, count, timeAtIndex); + var items = base.ContextMenuItemsInfo(); + + if (this.MarkerStyle == MarkerStyle.None) + { + items.Add(new ContextMenuItemInfo( + null, + "Show Markers", + new VisualizationCommand(() => this.MarkerStyle = MarkerStyle.Circle))); + } + else + { + items.Add(new ContextMenuItemInfo( + null, + "Hide Markers", + new VisualizationCommand(() => this.MarkerStyle = MarkerStyle.None))); + } + + return items; } /// diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/PipelineDiagnosticsVisualizationObject.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/PipelineDiagnosticsVisualizationObject.cs index d3bb21bee..a4c11437e 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/PipelineDiagnosticsVisualizationObject.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/PipelineDiagnosticsVisualizationObject.cs @@ -3,6 +3,8 @@ namespace Microsoft.Psi.Visualization.VisualizationObjects { + using System; + using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Runtime.Serialization; @@ -10,10 +12,11 @@ namespace Microsoft.Psi.Visualization.VisualizationObjects using System.Windows.Media; using Microsoft.Psi.Diagnostics; using Microsoft.Psi.Visualization; + using Microsoft.Psi.Visualization.Adapters; using Microsoft.Psi.Visualization.Helpers; - using Microsoft.Psi.Visualization.ViewModels; using Microsoft.Psi.Visualization.Views.Visuals2D; using Microsoft.Psi.Visualization.VisualizationPanels; + using Microsoft.Psi.Visualization.Windows; using Xceed.Wpf.Toolkit.PropertyGrid.Attributes; /// @@ -42,6 +45,8 @@ public class PipelineDiagnosticsVisualizationObject : StreamValueVisualizationOb private Color joinColor = Colors.LightSlateGray; private double infoTextSize = 12; + private int edgeUnderCursor = -1; + /// /// Enumeration of statistics available for heatmap visualization. /// @@ -423,24 +428,116 @@ public double InfoTextSize [IgnoreDataMember] public bool ModelDirty { get; set; } = false; + /// + public override List ContextMenuItemsInfo() + { + var items = new List(); + + // If the mouse is over an edge, add a menu item to expand the streams of the receiver. + if (this.edgeUnderCursor != -1) + { + var addDiagnosticsCommands = new ContextMenuItemInfo($"Add derived diagnostics streams for receiver {this.edgeUnderCursor}"); + items.Add(addDiagnosticsCommands); + + foreach (var receiverDiagnosticsStatistic in PipelineDiagnostics.ReceiverDiagnostics.AllStatistics) + { + addDiagnosticsCommands.SubItems.Add( + new ContextMenuItemInfo( + null, + receiverDiagnosticsStatistic, + new PsiCommand(() => this.AddReceiverDiagnosticsDerivedStream(this.edgeUnderCursor, receiverDiagnosticsStatistic)))); + } + } + + // Add a heatmap statistics context menu + var heatmapStatisticsItems = new ContextMenuItemInfo("Heatmap Statistics"); + items.Add(heatmapStatisticsItems); + + foreach (var heatmapStat in Enum.GetValues(typeof(HeatmapStats))) + { + var heatmapStatValue = (HeatmapStats)heatmapStat; + var heatmapStatName = heatmapStatValue switch + { + HeatmapStats.None => "None", + HeatmapStats.AvgMessageCreatedLatency => "Message Created Latency (Average)", + HeatmapStats.AvgMessageEmittedLatency => "Message Emitted Latency (Average)", + HeatmapStats.AvgMessageReceivedLatency => "Message Received Latency (Average)", + HeatmapStats.AvgDeliveryQueueSize => "Delivery Queue Size (Average)", + HeatmapStats.AvgMessageProcessTime => "Message Process Time (Average)", + HeatmapStats.TotalMessageEmittedCount => "Total Messages Emitted (Count)", + HeatmapStats.TotalMessageDroppedCount => "Total Messages Dropped (Count)", + HeatmapStats.TotalMessageDroppedPercentage => "Total Messages Dropped (%)", + HeatmapStats.TotalMessageProcessedCount => "Total Messages Processed (Count)", + HeatmapStats.AvgMessageSize => "Message Size (Average)", + _ => throw new NotImplementedException(), + }; + + heatmapStatisticsItems.SubItems.Add( + new ContextMenuItemInfo( + this.HeatmapStatistics == heatmapStatValue ? IconSourcePath.Checkmark : null, + heatmapStatName, + new PsiCommand(() => this.HeatmapStatistics = heatmapStatValue))); + } + + items.AddRange(base.ContextMenuItemsInfo()); + return items; + } + /// - /// Adds the derived receiver diagnostics streams for a specified receiver id. + /// Updates the edge currently under the cursor. + /// + /// Updates the edge under the cursor. + public void UpdateEdgeUnderCursor(int edgeUnderCursor) + { + this.edgeUnderCursor = edgeUnderCursor; + } + + /// + /// Adds a specified derived receiver diagnostics stream. /// /// The receiver id. - public void AddDerivedReceiverDiagnosticsStreams(int receiverId) + /// The name of the receiver diagnostics to use. + public void AddReceiverDiagnosticsDerivedStream(int receiverId, string receiverDiagnosticsStatistic) { - var partition = VisualizationContext.Instance + // Find the corresponding partition view model + var partitionViewModel = VisualizationContext.Instance .DatasetViewModel .CurrentSessionViewModel .PartitionViewModels .FirstOrDefault(p => p.Name == this.StreamBinding.PartitionName); - var pipelineDiagnosticsStreamTreeNode = partition - .FindStreamTreeNode(this.StreamBinding.StreamName) as PipelineDiagnosticsStreamTreeNode; - - var receiver = pipelineDiagnosticsStreamTreeNode.AddDerivedReceiverDiagnosticsChildren(receiverId); - partition.SelectNode(receiver.Path); - receiver.ExpandAll(); + // Find the pipeline diagnostics node + var diagnosticsNode = partitionViewModel.FindStreamTreeNode(this.StreamBinding.StreamName); + + // Figure out the type and extractor function for the statistic of interest + var receiverDiagnosticsStatisticType = this.GetTypeForReceiverDiagnosticsStatistic(receiverDiagnosticsStatistic); + var receiverDiagnosticsExtractorFunction = this.GetExtractorFunctionForReceiverDiagnosticsStatistic(receiverDiagnosticsStatistic); + + // Add the child node + var receiverDiagnosticsNode = diagnosticsNode.AddChild( + $"ReceiverDiagnostics.{receiverId}.{receiverDiagnosticsStatistic}", + diagnosticsNode.SourceStreamMetadata, + typeof(PipelineDiagnosticsToReceiverDiagnosticsMemberStreamAdapter<>).MakeGenericType(receiverDiagnosticsStatisticType), + new object[] { receiverId, receiverDiagnosticsExtractorFunction }); + + // Select it and expand the diagnostics node + if (receiverDiagnosticsNode != null) + { + partitionViewModel.SelectStreamTreeNode(receiverDiagnosticsNode.FullName); + diagnosticsNode.ExpandAll(); + } + else + { + Application.Current.Dispatcher.BeginInvoke((Action)(() => + { + new MessageBoxWindow( + Application.Current.MainWindow, + "Warning", + $"The receiver diagnostics statistic derived stream not added because derived streams with the same names already exist.", + "Close", + null).ShowDialog(); + })); + } } /// @@ -449,5 +546,55 @@ protected override void OnStreamUnbound() base.OnStreamUnbound(); this.ModelDirty = true; } + + private Type GetTypeForReceiverDiagnosticsStatistic(string receiverDiagnosticsStatistic) + => receiverDiagnosticsStatistic switch + { + nameof(PipelineDiagnostics.ReceiverDiagnostics.AvgMessageEmittedLatency) => typeof(double), + nameof(PipelineDiagnostics.ReceiverDiagnostics.AvgMessageCreatedLatency) => typeof(double), + nameof(PipelineDiagnostics.ReceiverDiagnostics.AvgMessageProcessTime) => typeof(double), + nameof(PipelineDiagnostics.ReceiverDiagnostics.AvgMessageReceivedLatency) => typeof(double), + nameof(PipelineDiagnostics.ReceiverDiagnostics.AvgMessageSize) => typeof(double), + nameof(PipelineDiagnostics.ReceiverDiagnostics.AvgDeliveryQueueSize) => typeof(double), + nameof(PipelineDiagnostics.ReceiverDiagnostics.LastMessageEmittedLatency) => typeof(double), + nameof(PipelineDiagnostics.ReceiverDiagnostics.LastMessageCreatedLatency) => typeof(double), + nameof(PipelineDiagnostics.ReceiverDiagnostics.LastMessageProcessTime) => typeof(double), + nameof(PipelineDiagnostics.ReceiverDiagnostics.LastMessageReceivedLatency) => typeof(double), + nameof(PipelineDiagnostics.ReceiverDiagnostics.LastMessageSize) => typeof(double), + nameof(PipelineDiagnostics.ReceiverDiagnostics.LastDeliveryQueueSize) => typeof(double), + nameof(PipelineDiagnostics.ReceiverDiagnostics.ReceiverIsThrottled) => typeof(bool), + nameof(PipelineDiagnostics.ReceiverDiagnostics.TotalMessageDroppedCount) => typeof(int), + nameof(PipelineDiagnostics.ReceiverDiagnostics.TotalMessageEmittedCount) => typeof(int), + nameof(PipelineDiagnostics.ReceiverDiagnostics.TotalMessageProcessedCount) => typeof(int), + nameof(PipelineDiagnostics.ReceiverDiagnostics.WindowMessageDroppedCount) => typeof(int), + nameof(PipelineDiagnostics.ReceiverDiagnostics.WindowMessageEmittedCount) => typeof(int), + nameof(PipelineDiagnostics.ReceiverDiagnostics.WindowMessageProcessedCount) => typeof(int), + _ => throw new ArgumentException($"Unknown receiver diagnostics statistic: {receiverDiagnosticsStatistic}") + }; + + private object GetExtractorFunctionForReceiverDiagnosticsStatistic(string receiverDiagnosticsStatistic) + => receiverDiagnosticsStatistic switch + { + nameof(PipelineDiagnostics.ReceiverDiagnostics.AvgMessageEmittedLatency) => (Func)(rd => rd.AvgMessageEmittedLatency), + nameof(PipelineDiagnostics.ReceiverDiagnostics.AvgMessageCreatedLatency) => (Func)(rd => rd.AvgMessageCreatedLatency), + nameof(PipelineDiagnostics.ReceiverDiagnostics.AvgMessageProcessTime) => (Func)(rd => rd.AvgMessageProcessTime), + nameof(PipelineDiagnostics.ReceiverDiagnostics.AvgMessageReceivedLatency) => (Func)(rd => rd.AvgMessageReceivedLatency), + nameof(PipelineDiagnostics.ReceiverDiagnostics.AvgMessageSize) => (Func)(rd => rd.AvgMessageSize), + nameof(PipelineDiagnostics.ReceiverDiagnostics.AvgDeliveryQueueSize) => (Func)(rd => rd.AvgDeliveryQueueSize), + nameof(PipelineDiagnostics.ReceiverDiagnostics.LastMessageEmittedLatency) => (Func)(rd => rd.LastMessageEmittedLatency), + nameof(PipelineDiagnostics.ReceiverDiagnostics.LastMessageCreatedLatency) => (Func)(rd => rd.LastMessageCreatedLatency), + nameof(PipelineDiagnostics.ReceiverDiagnostics.LastMessageProcessTime) => (Func)(rd => rd.LastMessageProcessTime), + nameof(PipelineDiagnostics.ReceiverDiagnostics.LastMessageReceivedLatency) => (Func)(rd => rd.LastMessageReceivedLatency), + nameof(PipelineDiagnostics.ReceiverDiagnostics.LastMessageSize) => (Func)(rd => rd.LastMessageSize), + nameof(PipelineDiagnostics.ReceiverDiagnostics.LastDeliveryQueueSize) => (Func)(rd => rd.LastDeliveryQueueSize), + nameof(PipelineDiagnostics.ReceiverDiagnostics.ReceiverIsThrottled) => (Func)(rd => rd.ReceiverIsThrottled), + nameof(PipelineDiagnostics.ReceiverDiagnostics.TotalMessageDroppedCount) => (Func)(rd => rd.TotalMessageDroppedCount), + nameof(PipelineDiagnostics.ReceiverDiagnostics.TotalMessageEmittedCount) => (Func)(rd => rd.TotalMessageEmittedCount), + nameof(PipelineDiagnostics.ReceiverDiagnostics.TotalMessageProcessedCount) => (Func)(rd => rd.TotalMessageProcessedCount), + nameof(PipelineDiagnostics.ReceiverDiagnostics.WindowMessageDroppedCount) => (Func)(rd => rd.WindowMessageDroppedCount), + nameof(PipelineDiagnostics.ReceiverDiagnostics.WindowMessageEmittedCount) => (Func)(rd => rd.WindowMessageEmittedCount), + nameof(PipelineDiagnostics.ReceiverDiagnostics.WindowMessageProcessedCount) => (Func)(rd => rd.WindowMessageProcessedCount), + _ => throw new ArgumentException($"Unknown receiver diagnostics statistic: {receiverDiagnosticsStatistic}") + }; } } diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/PosedModelFromFileVisualizationObject.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/PosedModelFromFileVisualizationObject.cs new file mode 100644 index 000000000..620730644 --- /dev/null +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/PosedModelFromFileVisualizationObject.cs @@ -0,0 +1,181 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.Psi.Visualization.VisualizationObjects +{ + using System; + using System.ComponentModel; + using System.Runtime.Serialization; + using System.Windows; + using System.Windows.Media; + using System.Windows.Media.Media3D; + using HelixToolkit.Wpf; + using MathNet.Spatial.Euclidean; + using Microsoft.Psi.Visualization.Extensions; + using Microsoft.Psi.Visualization.Windows; + using Vector3D = System.Windows.Media.Media3D.Vector3D; + + /// + /// Implements a visualization object for rendering a 3D model at a pose. + /// + /// + /// The desired model geometry is loaded from a file specified in the property. + /// Supported file formats include .obj, .stl, .3ds, .lwo, .objz, .off, and .ply. + /// + [VisualizationObject("Posed Model (from file)")] + public class PosedModelFromFileVisualizationObject : ModelVisual3DVisualizationObject + { + private readonly ModelImporter modelImporter = new (); + private readonly SolidColorBrush materialBrush = new (); + + private ModelVisual3D modelVisual; + private string modelFile; + private Color color = Colors.White; + private double opacity = 100; + private double scale = 1; + + /// + /// Initializes a new instance of the class. + /// + public PosedModelFromFileVisualizationObject() + { + this.modelImporter.DefaultMaterial = new DiffuseMaterial(this.materialBrush); + this.UpdateColor(); + this.UpdateOpacity(); + this.ResetModel(); + } + + /// + /// Gets or sets a value indicating which file to load for the model mesh. + /// + [DataMember] + [Description("The model file to load (full path).")] + public string ModelFile + { + get { return this.modelFile; } + set { this.Set(nameof(this.ModelFile), ref this.modelFile, value); } + } + + /// + /// Gets or sets the color. + /// + [DataMember] + [Description("The color of the model.")] + public Color Color + { + get { return this.color; } + set { this.Set(nameof(this.Color), ref this.color, value); } + } + + /// + /// Gets or sets the opacity. + /// + [DataMember] + [Description("The opacity of the model.")] + public double Opacity + { + get { return this.opacity; } + set { this.Set(nameof(this.Opacity), ref this.opacity, value); } + } + + /// + /// Gets or sets the scale. + /// + [DataMember] + [Description("The scale of the model.")] + public double Scale + { + get { return this.scale; } + set { this.Set(nameof(this.Scale), ref this.scale, value); } + } + + /// + public override void NotifyPropertyChanged(string propertyName) + { + if (propertyName == nameof(this.ModelFile)) + { + this.LoadModel(); + } + else if (propertyName == nameof(this.Color)) + { + this.UpdateColor(); + } + else if (propertyName == nameof(this.Opacity)) + { + this.UpdateOpacity(); + } + else if (propertyName == nameof(this.Scale)) + { + this.UpdateVisuals(); + } + else if (propertyName == nameof(this.Visible)) + { + this.UpdateVisibility(); + } + } + + /// + public override void UpdateData() + { + this.UpdateVisuals(); + this.UpdateVisibility(); + } + + private void UpdateVisuals() + { + if (this.CurrentData != null) + { + var currentPose = this.CurrentData.GetMatrix3D(); + currentPose.ScalePrepend(new Vector3D(this.scale, this.scale, this.scale)); + this.modelVisual.Transform = new MatrixTransform3D(currentPose); + } + } + + private void UpdateVisibility() + { + this.UpdateChildVisibility(this.modelVisual, this.Visible && this.CurrentData is not null); + } + + private void LoadModel() + { + try + { + this.modelVisual.Content = this.modelImporter.Load(this.modelFile); + } + catch (Exception e) + { + Application.Current.Dispatcher.BeginInvoke((Action)(() => + { + // Display an error message to the user. + new MessageBoxWindow(Application.Current.MainWindow, "Error Loading Model", e.Message, "Close", null).ShowDialog(); + })); + + this.ResetModel(); + throw; + } + + this.UpdateVisuals(); + } + + private void ResetModel() + { + if (this.ModelView.Children.Contains(this.modelVisual)) + { + this.ModelView.Children.Remove(this.modelVisual); + } + + this.modelVisual = new SphereVisual3D(); + this.UpdateVisibility(); + } + + private void UpdateOpacity() + { + this.materialBrush.Opacity = Math.Max(0, Math.Min(1.0, this.opacity / 100.0)); + } + + private void UpdateColor() + { + this.materialBrush.Color = this.color; + } + } +} \ No newline at end of file diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/StreamIntervalVisualizationObject{TData}.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/StreamIntervalVisualizationObject{TData}.cs index 44065d51f..f3cb26ecc 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/StreamIntervalVisualizationObject{TData}.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/StreamIntervalVisualizationObject{TData}.cs @@ -131,7 +131,7 @@ public string LegendFormat /// /// Gets a value indicating whether the visualization object is using summarization. /// - protected bool IsUsingSummarization => this.StreamBinding.SummarizerType != null; + protected bool IsUsingSummarization => this.StreamBinding.VisualizerSummarizerType != null; /// /// Gets the time interval of stream messages required for visualization. @@ -332,7 +332,7 @@ protected override void OnPanelPropertyChanged(object sender, PropertyChangedEve if (e.PropertyName == nameof(this.Panel.Width)) { // And if we are summarizing - if (this.StreamBinding?.SummarizerType != null) + if (this.StreamBinding?.VisualizerSummarizerType != null) { // Then refresh the data as the sampling rate for summarization will be different this.RefreshData(); diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/StreamVisualizationObject{TData}.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/StreamVisualizationObject{TData}.cs index fb54e9894..dfbaeb94a 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/StreamVisualizationObject{TData}.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/StreamVisualizationObject{TData}.cs @@ -4,15 +4,16 @@ namespace Microsoft.Psi.Visualization.VisualizationObjects { using System; + using System.Collections.Generic; using System.ComponentModel; using System.Runtime.Serialization; using System.Windows; using GalaSoft.MvvmLight.CommandWpf; using Microsoft.Psi; + using Microsoft.Psi.Data; using Microsoft.Psi.PsiStudio.TypeSpec; using Microsoft.Psi.Visualization; using Microsoft.Psi.Visualization.Data; - using Microsoft.Psi.Visualization.Helpers; using Microsoft.Psi.Visualization.ViewModels; using Microsoft.Psi.Visualization.Windows; @@ -60,9 +61,9 @@ public StreamVisualizationObject() /// Gets the stream name. /// [Browsable(true)] - [DisplayName("Stream Name")] + [DisplayName("Source Stream Name")] [IgnoreDataMember] - public string StreamName => this.StreamBinding?.StreamName; + public string SourceStreamName => this.StreamBinding?.SourceStreamName; /// /// Gets the stream name. @@ -83,17 +84,7 @@ public StreamVisualizationObject() [Browsable(false)] [IgnoreDataMember] public RelayCommand ToggleSnapToStreamCommand - { - get - { - if (this.toggleSnapToStreamCommand == null) - { - this.toggleSnapToStreamCommand = new RelayCommand(() => this.ToggleSnapToStream()); - } - - return this.toggleSnapToStreamCommand; - } - } + => this.toggleSnapToStreamCommand ??= new RelayCommand(() => this.ToggleSnapToStream()); /// /// Gets the zoom to stream command. @@ -101,19 +92,9 @@ public RelayCommand ToggleSnapToStreamCommand [Browsable(false)] [IgnoreDataMember] public RelayCommand ZoomToStreamCommand - { - get - { - if (this.zoomToStreamCommand == null) - { - this.zoomToStreamCommand = new RelayCommand( + => this.zoomToStreamCommand ??= new RelayCommand( () => this.Container.Navigator.Zoom(this.StreamSource.StreamMetadata.FirstMessageOriginatingTime, this.StreamSource.StreamMetadata.LastMessageOriginatingTime), () => this.StreamSource != null); - } - - return this.zoomToStreamCommand; - } - } /// /// Gets a value indicating whether the visualization object has a current value. @@ -171,36 +152,28 @@ private set } } - /// - /// Gets the adapter type. - /// - [Browsable(false)] - [IgnoreDataMember] - public Type StreamAdapterType => this.StreamBinding?.StreamAdapterType; - /// /// Gets the adapter type name (used by property browser). /// - [Browsable(true)] - [DisplayName("Stream Adapter")] - [Description("The stream adapter used by the visualizer.")] + [DisplayName("Stream Adapter Type")] + [Description("The type of stream adapter used by the visualizer.")] [IgnoreDataMember] - public string StreamAdapterDisplayName => TypeSpec.Simplify(this.StreamBinding?.StreamAdapterType?.FullName); + public string StreamAdapterTypeDisplayString => TypeSpec.Simplify(this.StreamBinding?.VisualizerStreamAdapterType?.AssemblyQualifiedName); /// /// Gets the summarizer type. /// [Browsable(false)] [IgnoreDataMember] - public Type SummarizerType => this.StreamBinding?.SummarizerType; + public Type SummarizerType => this.StreamBinding?.VisualizerSummarizerType; /// /// Gets the summarizer type name (used by property browser). /// [Browsable(true)] - [DisplayName("Summarizer")] + [DisplayName("Summarizer Type")] [IgnoreDataMember] - public string SummarizerTypeDisplayName => TypeSpec.Simplify(this.StreamBinding?.SummarizerType?.FullName); + public string SummarizerTypeDisplayString => TypeSpec.Simplify(this.StreamBinding?.VisualizerSummarizerType?.FullName); /// [Browsable(false)] @@ -229,10 +202,10 @@ public override string IconSource { if (!this.IsBound) { - if (this.StreamBinding.IsDerived) + if (this.StreamBinding.IsBindingToDerivedStream) { // Stream member unbound - return IconSourcePath.StreamMemberUnbound; + return IconSourcePath.DerivedStreamUnbound; } else { @@ -242,10 +215,10 @@ public override string IconSource } else if (this.IsSnappedToStream) { - if (this.StreamBinding.IsDerived) + if (this.StreamBinding.IsBindingToDerivedStream) { // Snap to stream member - return this.IsLive ? IconSourcePath.StreamMemberSnapLive : IconSourcePath.StreamMemberSnap; + return this.IsLive ? IconSourcePath.DerivedStreamSnapLive : IconSourcePath.DerivedStreamSnap; } else { @@ -253,10 +226,10 @@ public override string IconSource return this.IsLive ? IconSourcePath.SnapToStreamLive : IconSourcePath.SnapToStream; } } - else if (this.StreamBinding.IsDerived) + else if (this.StreamBinding.IsBindingToDerivedStream) { // Stream member - return this.IsLive ? IconSourcePath.StreamMemberLive : IconSourcePath.StreamMember; + return this.IsLive ? IconSourcePath.DerivedStreamLive : IconSourcePath.DerivedStream; } else { @@ -308,6 +281,23 @@ public override string IconSource } }; + /// + public override List ContextMenuItemsInfo() + { + var items = base.ContextMenuItemsInfo(); + if (this.CanSnapToStream) + { + items.Insert( + 0, + new ContextMenuItemInfo( + IconSourcePath.SnapToStream, + this.ToggleSnapToStreamCommandText, + this.ToggleSnapToStreamCommand)); + } + + return items; + } + /// /// Sets the current value for the visualization object. /// @@ -434,9 +424,7 @@ public void UpdateStreamSource(SessionViewModel sessionViewModel) /// Function that returns an index given a time. /// Best matching index or -1 if no qualifying match was found. protected virtual int GetIndexForTime(DateTime currentTime, int count, Func timeAtIndex) - { - return IndexHelper.GetIndexForTime(currentTime, count, timeAtIndex); - } + => IndexHelper.GetIndexForTime(currentTime, count, timeAtIndex); /// protected override void OnAddToPanel() diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/VisualizationContainer.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/VisualizationContainer.cs index ba2737b43..430dbb9b6 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/VisualizationContainer.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/VisualizationContainer.cs @@ -26,7 +26,7 @@ namespace Microsoft.Psi.Visualization.VisualizationObjects /// /// Implements the container where all visualization panels are hosted. The is the root UI element for visualizations. /// - public class VisualizationContainer : ObservableObject + public class VisualizationContainer : ObservableObject, IContextMenuItemsSource, IDisposable { // Property names used in the layout (*.plo) files private const string LayoutPropertyName = "Layout"; @@ -186,21 +186,7 @@ public bool ShowNavigator /// [IgnoreDataMember] public RelayCommand DeleteVisualizationPanelCommand - { - get - { - if (this.deleteVisualizationPanelCommand == null) - { - this.deleteVisualizationPanelCommand = new RelayCommand( - o => - { - this.RemovePanel(o); - }); - } - - return this.deleteVisualizationPanelCommand; - } - } + => this.deleteVisualizationPanelCommand ??= new RelayCommand(o => this.RemovePanel(o)); /// /// Loads a visualization layout from the specified file. @@ -270,6 +256,25 @@ public static VisualizationContainer Load(string filename, string layoutName, Vi } } + /// + public void Dispose() + { + // Ensure playback is stopped + this.Navigator.SetCursorMode(CursorMode.Manual); + + // Clear container to give all panels a chance to clean up + this.Clear(); + } + + /// + public List ContextMenuItemsInfo() + => new () + { + new ContextMenuItemInfo(IconSourcePath.ZoomToSelection, "Zoom to Selection", this.ZoomToSelectionCommand), + new ContextMenuItemInfo(IconSourcePath.ClearSelection, "Clear Selection", this.ClearSelectionCommand), + new ContextMenuItemInfo(IconSourcePath.ZoomToSession, "Zoom to Session Extents", this.ZoomToSessionExtentsCommand), + }; + /// /// Adds a new panel to the container. /// @@ -480,6 +485,9 @@ public void UnbindVisualizationObjectsFromStore(string storeName, string storePa /// The currently active session view model. public void UpdateStreamSources(SessionViewModel sessionViewModel) { + // First, ensure any required derived stream tree nodes exist. + sessionViewModel?.EnsureDerivedStreamTreeNodesExist(this); + foreach (var panel in this.Panels) { // Update the stream sources for the panel @@ -521,6 +529,19 @@ public void GoToTime() return (false, "The specified date-time is outside the range of the current session."); } } + else if (long.TryParse(value, out var ticks)) + { + var ticksDateTime = new DateTime(ticks); + if (ticksDateTime >= this.Navigator.DataRange.StartTime && + ticksDateTime <= this.Navigator.DataRange.EndTime) + { + return (true, string.Empty); + } + else + { + return (false, "The specified date-time is outside the range of the current session."); + } + } else { return (false, "Cannot convert the specified time to a valid date time."); @@ -529,7 +550,15 @@ public void GoToTime() if (getTime.ShowDialog() == true) { - var cursor = DateTime.Parse(getTime.ParameterValue); + var cursor = default(DateTime); + if (DateTime.TryParse(getTime.ParameterValue, out var dateTime)) + { + cursor = dateTime; + } + else if (long.TryParse(getTime.ParameterValue, out var ticks)) + { + cursor = new DateTime(ticks); + } // If the cursor falls outside the current view range, shift the view range if (cursor <= this.Navigator.ViewRange.StartTime) diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/VisualizationObject.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/VisualizationObject.cs index 47a9c7b1b..583173284 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/VisualizationObject.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/VisualizationObject.cs @@ -4,6 +4,7 @@ namespace Microsoft.Psi.Visualization.VisualizationObjects { using System; + using System.Collections.Generic; using System.ComponentModel; using System.Runtime.Serialization; using System.Windows; @@ -18,7 +19,7 @@ namespace Microsoft.Psi.Visualization.VisualizationObjects /// /// Provides an abstract base class for visualization objects. /// - public abstract class VisualizationObject : ObservableTreeNodeObject + public abstract class VisualizationObject : ObservableTreeNodeObject, IContextMenuItemsSource { /// /// The name of the visualization object. @@ -258,6 +259,24 @@ public RelayCommand ToggleVisibilityCommand [IgnoreDataMember] protected virtual IContractResolver ContractResolver => null; + /// + public virtual List ContextMenuItemsInfo() + => new () + { + // Add the show/hide command + new ContextMenuItemInfo( + IconSourcePath.ToggleVisibility, + this.Visible ? "Hide Visualizer" : "Show Visualizer", + this.ToggleVisibilityCommand), + + // Add the remove from panel command + new ContextMenuItemInfo( + IconSourcePath.RemovePanel, + "Remove Visualizer", + this.Panel.DeleteVisualizationCommand, + commandParameter: this), + }; + /// /// Snaps or unsnaps the navigation cursor to the visualization object. /// diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/XYValueVisualizationObject.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/XYValueVisualizationObject.cs index bfe71c60b..e0acc02e5 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/XYValueVisualizationObject.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationObjects/XYValueVisualizationObject.cs @@ -6,7 +6,6 @@ namespace Microsoft.Psi.Visualization.VisualizationObjects using System; using System.ComponentModel; using System.Runtime.Serialization; - using System.Windows; using Microsoft.Psi.Visualization.VisualizationPanels; /// diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationPanels/InstantVisualizationContainer.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationPanels/InstantVisualizationContainer.cs index a15cc7856..0a6c8ed0e 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationPanels/InstantVisualizationContainer.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationPanels/InstantVisualizationContainer.cs @@ -7,6 +7,7 @@ namespace Microsoft.Psi.Visualization.VisualizationPanels using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; + using System.Linq; using System.Runtime.Serialization; using System.Windows; using Microsoft.Psi.Visualization.Helpers; @@ -79,19 +80,27 @@ public ObservableCollection Panels /// public override List CompatiblePanelTypes => new (); + /// + public override List ContextMenuItemsInfo() + => new () + { + new ContextMenuItemInfo(IconSourcePath.InstantContainerAddCellLeft, $"Insert Cell to the Left", this.InsertCellCommand(true)), + new ContextMenuItemInfo(IconSourcePath.InstantContainerAddCellRight, $"Insert Cell to the Right", this.InsertCellCommand(false)), + new ContextMenuItemInfo(null, $"Remove Cell", this.CreateRemoveCellCommand(null)), + new ContextMenuItemInfo(IconSourcePath.InstantContainerRemoveCell, $"Remove {this.Name}", this.RemovePanelCommand), + }; + /// - /// Gets the increase cell count command. + /// Inserts a cell in the visualization container view to the left or to the right of the current panel. /// - /// The panel that the child panel should be inserted next to. /// True if the panel should be inserted to the left of panel, otherwise false. /// The complete relay command. - public PsiCommand CreateIncreaseCellCountCommand(VisualizationPanel panel, bool insertOnLeft) + public PsiCommand InsertCellCommand(bool insertOnLeft) { - int panelIndex = this.panels.IndexOf(panel); - - return new PsiCommand( - () => this.IncreaseCellCount(insertOnLeft ? panelIndex : panelIndex + 1), - this.Panels.Count < MaxCells); + var currentPanelIndex = this.Panels.IndexOf(this.Panels.First(p => p.IsCurrentPanel)); + return new ( + () => this.IncreaseCellCount(insertOnLeft ? currentPanelIndex : currentPanelIndex + 1), + this.Panels.Count < MaxCells); } /// diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationPanels/InstantVisualizationPanel.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationPanels/InstantVisualizationPanel.cs index b2d441d98..3d87d4a24 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationPanels/InstantVisualizationPanel.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationPanels/InstantVisualizationPanel.cs @@ -3,10 +3,14 @@ namespace Microsoft.Psi.Visualization.VisualizationPanels { + using System.Collections.Generic; using System.ComponentModel; + using System.Linq; using System.Runtime.Serialization; + using System.Windows; + using GalaSoft.MvvmLight.CommandWpf; using Microsoft.Psi.Visualization.VisualizationObjects; - using Xceed.Wpf.Toolkit.PropertyGrid.Attributes; + using Microsoft.Psi.Visualization.Windows; /// /// Represents the base class that instant visualization panels derive from. @@ -53,6 +57,41 @@ public int RelativeWidth set { this.Set(nameof(this.RelativeWidth), ref this.relativeWidth, value); } } + /// + public override List ContextMenuItemsInfo() + { + var items = new List(); + + // Add Set Cursor Epsilon menu with sub-menu items + var setCursorEpsilonItems = new ContextMenuItemInfo("Set Default Cursor Epsilon"); + + setCursorEpsilonItems.SubItems.Add( + new ContextMenuItemInfo( + null, + "Infinite Past", + new RelayCommand(() => this.UpdateDefaultCursorEpsilon("Infinite Past", int.MaxValue, 0), true))); + setCursorEpsilonItems.SubItems.Add( + new ContextMenuItemInfo( + null, + "Last 5 seconds", + new RelayCommand(() => this.UpdateDefaultCursorEpsilon("Last 5 seconds", 5000, 0), true))); + setCursorEpsilonItems.SubItems.Add( + new ContextMenuItemInfo( + null, + "Last 1 second", + new RelayCommand(() => this.UpdateDefaultCursorEpsilon("Last 1 second", 1000, 0), true))); + setCursorEpsilonItems.SubItems.Add( + new ContextMenuItemInfo( + null, + "Last 50 milliseconds", + new RelayCommand(() => this.UpdateDefaultCursorEpsilon("Last 50 milliseconds", 50, 0), true))); + + items.Add(setCursorEpsilonItems); + + items.AddRange(base.ContextMenuItemsInfo()); + return items; + } + /// public override void AddVisualizationObject(VisualizationObject visualizationObject) { @@ -61,5 +100,32 @@ public override void AddVisualizationObject(VisualizationObject visualizationObj visualizationObject.CursorEpsilonNegMs = this.defaultCursorEpsilonNegMs; visualizationObject.CursorEpsilonPosMs = this.defaultCursorEpsilonPosMs; } + + private void UpdateDefaultCursorEpsilon(string name, int negMs, int posMs) + { + this.DefaultCursorEpsilonNegMs = negMs; + this.DefaultCursorEpsilonPosMs = posMs; + + var anyVisualizersWithDifferentCursorEpsilon = + this.VisualizationObjects.Any(vo => vo.CursorEpsilonNegMs != 50 || vo.CursorEpsilonPosMs != 0); + + if (anyVisualizersWithDifferentCursorEpsilon) + { + var result = new MessageBoxWindow( + Application.Current.MainWindow, + "Update visualizers?", + $"Some of the visualizers in this panel have a cursor epsilon that is different from {name}. Would you also like to change the cursor epsilon for these visualizers to {name}?", + "Yes", + "No").ShowDialog(); + if (result == true) + { + foreach (var visualizationObject in this.VisualizationObjects) + { + visualizationObject.CursorEpsilonNegMs = negMs; + visualizationObject.CursorEpsilonPosMs = posMs; + } + } + } + } } } \ No newline at end of file diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationPanels/TimelineVisualizationPanel.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationPanels/TimelineVisualizationPanel.cs index 53cb3f7b2..67bf4f371 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationPanels/TimelineVisualizationPanel.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationPanels/TimelineVisualizationPanel.cs @@ -15,6 +15,7 @@ namespace Microsoft.Psi.Visualization.VisualizationPanels using System.Windows.Input; using System.Windows.Media; using GalaSoft.MvvmLight.CommandWpf; + using Microsoft.Psi.Data; using Microsoft.Psi.Visualization; using Microsoft.Psi.Visualization.Common; using Microsoft.Psi.Visualization.Controls; @@ -80,17 +81,7 @@ public TimelineVisualizationPanel() [Browsable(false)] [IgnoreDataMember] public RelayCommand ShowHideLegendCommand - { - get - { - if (this.showHideLegendCommand == null) - { - this.showHideLegendCommand = new RelayCommand(() => this.ShowLegend = !this.ShowLegend); - } - - return this.showHideLegendCommand; - } - } + => this.showHideLegendCommand ??= new RelayCommand(() => this.ShowLegend = !this.ShowLegend); /// /// Gets the clear selection command. @@ -98,19 +89,9 @@ public RelayCommand ShowHideLegendCommand [Browsable(false)] [IgnoreDataMember] public RelayCommand ClearSelectionCommand - { - get - { - if (this.clearSelectionCommand == null) - { - this.clearSelectionCommand = new RelayCommand( - () => this.Container.Navigator.ClearSelection(), - () => this.Container.Navigator.CanClearSelection()); - } - - return this.clearSelectionCommand; - } - } + => this.clearSelectionCommand ??= new RelayCommand( + () => this.Container.Navigator.ClearSelection(), + () => this.Container.Navigator.CanClearSelection()); /// /// Gets the mouse right button down command. @@ -118,21 +99,7 @@ public RelayCommand ClearSelectionCommand [Browsable(false)] [IgnoreDataMember] public RelayCommand ViewportLoadedCommand - { - get - { - if (this.viewportLoadedCommand == null) - { - this.viewportLoadedCommand = new RelayCommand( - e => - { - this.viewport = e.Source as Grid; - }); - } - - return this.viewportLoadedCommand; - } - } + => this.viewportLoadedCommand ??= new RelayCommand(e => this.viewport = e.Source as Grid); /// /// Gets the items control size changed command. @@ -140,17 +107,7 @@ public RelayCommand ViewportLoadedCommand [Browsable(false)] [IgnoreDataMember] public RelayCommand ViewportSizeChangedCommand - { - get - { - if (this.viewportSizeChangedCommand == null) - { - this.viewportSizeChangedCommand = new RelayCommand(e => this.OnViewportSizeChanged(e)); - } - - return this.viewportSizeChangedCommand; - } - } + => this.viewportSizeChangedCommand ??= new RelayCommand(e => this.OnViewportSizeChanged(e)); /// /// Gets the mouse left button down command. @@ -158,39 +115,29 @@ public RelayCommand ViewportSizeChangedCommand [Browsable(false)] [IgnoreDataMember] public override RelayCommand MouseLeftButtonDownCommand - { - get - { - if (this.mouseLeftButtonDownCommand == null) + => this.mouseLeftButtonDownCommand ??= new RelayCommand( + e => { - this.mouseLeftButtonDownCommand = new RelayCommand( - e => + // Set the current panel on click + if (!this.IsCurrentPanel) + { + this.IsTreeNodeSelected = true; + this.Container.CurrentPanel = this; + + // If the panel contains any visualization objects, set the first one as selected. + if (this.VisualizationObjects.Any()) { - // Set the current panel on click - if (!this.IsCurrentPanel) - { - this.IsTreeNodeSelected = true; - this.Container.CurrentPanel = this; - - // If the panel contains any visualization objects, set the first one as selected. - if (this.VisualizationObjects.Any()) - { - this.VisualizationObjects[0].IsTreeNodeSelected = true; - } - } - - if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift)) - { - var cursor = this.Navigator.Cursor; - this.Navigator.SelectionRange.Set(cursor, this.Navigator.SelectionRange.EndTime >= cursor ? this.Navigator.SelectionRange.EndTime : DateTime.MaxValue); - e.Handled = true; - } - }); - } + this.VisualizationObjects[0].IsTreeNodeSelected = true; + } + } - return this.mouseLeftButtonDownCommand; - } - } + if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift)) + { + var cursor = this.Navigator.Cursor; + this.Navigator.SelectionRange.Set(cursor, this.Navigator.SelectionRange.EndTime >= cursor ? this.Navigator.SelectionRange.EndTime : DateTime.MaxValue); + e.Handled = true; + } + }); /// /// Gets the mouse right button down command. @@ -198,26 +145,16 @@ public override RelayCommand MouseLeftButtonDownCommand [Browsable(false)] [IgnoreDataMember] public RelayCommand MouseRightButtonDownCommand - { - get - { - if (this.mouseRightButtonDownCommand == null) + => this.mouseRightButtonDownCommand ??= new RelayCommand( + e => { - this.mouseRightButtonDownCommand = new RelayCommand( - e => - { - if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift)) - { - var time = this.Navigator.Cursor; - this.Navigator.SelectionRange.Set(this.Navigator.SelectionRange.StartTime <= time ? this.Navigator.SelectionRange.StartTime : DateTime.MinValue, time); - e.Handled = true; - } - }); - } - - return this.mouseRightButtonDownCommand; - } - } + if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift)) + { + var time = this.Navigator.Cursor; + this.Navigator.SelectionRange.Set(this.Navigator.SelectionRange.StartTime <= time ? this.Navigator.SelectionRange.StartTime : DateTime.MinValue, time); + e.Handled = true; + } + }); /// /// Gets the mouse move command. @@ -225,28 +162,18 @@ public RelayCommand MouseRightButtonDownCommand [Browsable(false)] [IgnoreDataMember] public RelayCommand MouseMoveCommand - { - get - { - if (this.mouseMoveCommand == null) + => this.mouseMoveCommand ??= new RelayCommand( + e => { - this.mouseMoveCommand = new RelayCommand( - e => - { - // Get the current mouse position - Point newMousePosition = e.GetPosition(this.viewport); + // Get the current mouse position + Point newMousePosition = e.GetPosition(this.viewport); - // Get the current scale factor between the axes logical bounds and the items control size. - double scaleY = (this.YAxis.Maximum - this.YAxis.Minimum) / this.viewport.ActualHeight; + // Get the current scale factor between the axes logical bounds and the items control size. + double scaleY = (this.YAxis.Maximum - this.YAxis.Minimum) / this.viewport.ActualHeight; - // Set the mouse position in locical/image co-ordinates - this.MousePosition = new TimelinePanelMousePosition(this.GetTimeAtMousePointer(e, false), this.YAxis.Maximum - newMousePosition.Y * scaleY /*+ this.YAxis.Minimum*/); - }); - } - - return this.mouseMoveCommand; - } - } + // Set the mouse position in locical/image co-ordinates + this.MousePosition = new TimelinePanelMousePosition(this.GetTimeAtMousePointer(e, false), this.YAxis.Maximum - newMousePosition.Y * scaleY /*+ this.YAxis.Minimum*/); + }); /// /// Gets the set auto axis compute mode command for both the X and Y axes. @@ -254,20 +181,7 @@ public RelayCommand MouseMoveCommand [Browsable(false)] [IgnoreDataMember] public RelayCommand SetAutoAxisComputeModeCommand - { - get - { - if (this.setAutoAxisComputeModeCommand == null) - { - this.setAutoAxisComputeModeCommand = new RelayCommand(() => - { - this.AxisComputeMode = AxisComputeMode.Auto; - }); - } - - return this.setAutoAxisComputeModeCommand; - } - } + => this.setAutoAxisComputeModeCommand ??= new RelayCommand(() => this.AxisComputeMode = AxisComputeMode.Auto); /// /// Gets or sets the Y Axis for the panel. @@ -328,7 +242,7 @@ public AxisComputeMode AxisComputeMode [PropertyOrder(4)] [DisplayName("Mouse Position")] [Description("The position of the mouse within the Timeline Visualization panel.")] - public string MousePositionString => $"{DateTimeFormatHelper.FormatTime(this.mousePosition.X)}, {this.mousePosition.Y:F}"; + public string MousePositionString => $"{DateTimeHelper.FormatTime(this.mousePosition.X)}, {this.mousePosition.Y:F}"; /// /// Gets or sets the mouse position in the panel. @@ -429,6 +343,25 @@ public double ThresholdRectangleHeight /// public override List CompatiblePanelTypes => new () { VisualizationPanelType.Timeline }; + /// + public override List ContextMenuItemsInfo() + { + var items = new List() + { + // The show/hide legend menu + new ContextMenuItemInfo(IconSourcePath.Legend, this.ShowLegend ? $"Hide Legend" : $"Show Legend", this.ShowHideLegendCommand), + new ContextMenuItemInfo( + null, + "Auto-Fit Axes", + this.SetAutoAxisComputeModeCommand, + isEnabled: this.AxisComputeMode == AxisComputeMode.Manual), + null, + }; + + items.AddRange(base.ContextMenuItemsInfo()); + return items; + } + /// /// Gets the time at the mouse pointer, optionally adjusting for visualization object snap. /// @@ -449,7 +382,7 @@ public DateTime GetTimeAtMousePointer(MouseEventArgs mouseEventArgs, bool useSna DateTime? snappedTime = null; if (useSnap == true && VisualizationContext.Instance.VisualizationContainer.SnapToVisualizationObject is IStreamVisualizationObject snapToVisualizationObject) { - snappedTime = DataManager.Instance.GetTimeOfNearestMessage(snapToVisualizationObject.StreamSource, time, NearestMessageType.Nearest); + snappedTime = DataManager.Instance.GetTimeOfNearestMessage(snapToVisualizationObject.StreamSource, time, NearestType.Nearest); } return snappedTime ?? time; diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationPanels/VisualizationPanel.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationPanels/VisualizationPanel.cs index c5bf0745e..67f049245 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationPanels/VisualizationPanel.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationPanels/VisualizationPanel.cs @@ -27,7 +27,7 @@ namespace Microsoft.Psi.Visualization.VisualizationPanels /// /// Represents the base class that visualization panels derive from. /// - public abstract class VisualizationPanel : ObservableTreeNodeObject + public abstract class VisualizationPanel : ObservableTreeNodeObject, IContextMenuItemsSource { // The minimum height of a Visualization Panel private const double MinHeight = 10; @@ -109,17 +109,7 @@ public VisualizationPanel() [Browsable(false)] [IgnoreDataMember] public RelayCommand ToggleAllVisualizersVisibilityCommand - { - get - { - if (this.toggleAllVisualizersVisibilityCommand == null) - { - this.toggleAllVisualizersVisibilityCommand = new RelayCommand(() => this.ToggleAllVisualizationObjectsVisibility()); - } - - return this.toggleAllVisualizersVisibilityCommand; - } - } + => this.toggleAllVisualizersVisibilityCommand ??= new RelayCommand(() => this.ToggleAllVisualizationObjectsVisibility()); /// /// Gets the toggle visibility command. @@ -127,17 +117,7 @@ public RelayCommand ToggleAllVisualizersVisibilityCommand [Browsable(false)] [IgnoreDataMember] public RelayCommand ToggleVisibilityCommand - { - get - { - if (this.toggleVisibilityCommand == null) - { - this.toggleVisibilityCommand = new RelayCommand(() => this.Container.TogglePanelVisibility(this)); - } - - return this.toggleVisibilityCommand; - } - } + => this.toggleVisibilityCommand ??= new RelayCommand(() => this.Container.TogglePanelVisibility(this)); /// /// Gets the remove panel command. @@ -145,17 +125,7 @@ public RelayCommand ToggleVisibilityCommand [Browsable(false)] [IgnoreDataMember] public RelayCommand RemovePanelCommand - { - get - { - if (this.removePanelCommand == null) - { - this.removePanelCommand = new RelayCommand(() => this.Container.RemovePanel(this)); - } - - return this.removePanelCommand; - } - } + => this.removePanelCommand ??= new RelayCommand(() => this.Container.RemovePanel(this)); /// /// Gets the clear panel command. @@ -163,17 +133,7 @@ public RelayCommand RemovePanelCommand [Browsable(false)] [IgnoreDataMember] public RelayCommand ClearPanelCommand - { - get - { - if (this.clearPanelCommand == null) - { - this.clearPanelCommand = new RelayCommand(() => this.Clear()); - } - - return this.clearPanelCommand; - } - } + => this.clearPanelCommand ??= new RelayCommand(() => this.Clear()); /// /// Gets the mouse left button down command. @@ -181,33 +141,23 @@ public RelayCommand ClearPanelCommand [Browsable(false)] [IgnoreDataMember] public virtual RelayCommand MouseLeftButtonDownCommand - { - get - { - if (this.mouseLeftButtonDownCommand == null) + => this.mouseLeftButtonDownCommand ??= new RelayCommand( + e => { - this.mouseLeftButtonDownCommand = new RelayCommand( - e => - { - // Set the current panel on click - if (!this.IsCurrentPanel) - { - // Set the current panel to this panel - this.IsTreeNodeSelected = true; - this.Container.CurrentPanel = this; - - // If the panel contains any visualization objects, set the first one as selected. - if (this.VisualizationObjects.Any()) - { - this.VisualizationObjects[0].IsTreeNodeSelected = true; - } - } - }); - } + // Set the current panel on click + if (!this.IsCurrentPanel) + { + // Set the current panel to this panel + this.IsTreeNodeSelected = true; + this.Container.CurrentPanel = this; - return this.mouseLeftButtonDownCommand; - } - } + // If the panel contains any visualization objects, set the first one as selected. + if (this.VisualizationObjects.Any()) + { + this.VisualizationObjects[0].IsTreeNodeSelected = true; + } + } + }); /// /// Gets the resize panel command. @@ -215,17 +165,7 @@ public virtual RelayCommand MouseLeftButtonDownCommand [Browsable(false)] [IgnoreDataMember] public RelayCommand ResizePanelCommand - { - get - { - if (this.resizePanelCommand == null) - { - this.resizePanelCommand = new RelayCommand(o => this.Height = Math.Max(this.Height + o.VerticalChange, MinHeight)); - } - - return this.resizePanelCommand; - } - } + => this.resizePanelCommand ??= new RelayCommand(o => this.Height = Math.Max(this.Height + o.VerticalChange, MinHeight)); /// /// Gets or sets the name of the visualization panel name. @@ -327,18 +267,7 @@ public VisualizationObject CurrentVisualizationObject /// [Browsable(false)] [IgnoreDataMember] - public DataTemplate DefaultViewTemplate - { - get - { - if (this.defaultViewTemplate == null) - { - this.defaultViewTemplate = this.CreateDefaultViewTemplate(); - } - - return this.defaultViewTemplate; - } - } + public DataTemplate DefaultViewTemplate => this.defaultViewTemplate ??= this.CreateDefaultViewTemplate(); /// /// Gets or sets the visual margin for the panel. @@ -399,18 +328,7 @@ public Thickness VisualMargin [Browsable(false)] [IgnoreDataMember] public RelayCommand ZoomToPanelCommand - { - get - { - if (this.zoomToPanelCommand == null) - { - this.zoomToPanelCommand = new RelayCommand( - () => this.ZoomToPanel()); - } - - return this.zoomToPanelCommand; - } - } + => this.zoomToPanelCommand ??= new RelayCommand(() => this.ZoomToPanel()); /// /// Gets the delete visualization command. @@ -418,17 +336,73 @@ public RelayCommand ZoomToPanelCommand [Browsable(false)] [IgnoreDataMember] public RelayCommand DeleteVisualizationCommand - { - get - { - if (this.deleteVisualizationCommand == null) - { - this.deleteVisualizationCommand = new RelayCommand( - (o) => this.RemoveVisualizationObject(o)); - } + => this.deleteVisualizationCommand ??= new RelayCommand(o => this.RemoveVisualizationObject(o)); - return this.deleteVisualizationCommand; - } + /// + public virtual List ContextMenuItemsInfo() + { + var commands = new List(); + + if (this.VisualizationObjects.Count > 0) + { + var visible = this.VisualizationObjects.Any(vo => vo.Visible); + commands.Add( + new ContextMenuItemInfo( + IconSourcePath.ToggleVisibility, + visible ? "Hide All Visualizers" : "Show All Visualizers", + this.ToggleAllVisualizersVisibilityCommand, + isEnabled: true)); + } + + commands.Add( + new ContextMenuItemInfo( + IconSourcePath.ClearPanel, + $"Remove All Visualizers", + this.ClearPanelCommand, + isEnabled: this.VisualizationObjects.Count > 0)); + + // Add copy to clipboard menu with sub-menu items + var copyToClipboardCommands = new ContextMenuItemInfo("Copy to Clipboard"); + + copyToClipboardCommands.SubItems.Add( + new ContextMenuItemInfo( + null, + "Cursor Time", + this.Navigator.CopyToClipboardCommand, + isEnabled: true, + commandParameter: this.Navigator.Cursor.ToString("M/d/yyyy HH:mm:ss.ffff"))); + copyToClipboardCommands.SubItems.Add( + new ContextMenuItemInfo( + null, + "Cursor Time (as Ticks)", + this.Navigator.CopyToClipboardCommand, + isEnabled: true, + commandParameter: this.Navigator.Cursor.Ticks.ToString())); + copyToClipboardCommands.SubItems.Add( + new ContextMenuItemInfo( + null, + "Session Name", + this.Navigator.CopyToClipboardCommand, + isEnabled: VisualizationContext.Instance.DatasetViewModel?.CurrentSessionViewModel != null, + commandParameter: VisualizationContext.Instance.DatasetViewModel.CurrentSessionViewModel.Name.ToString())); + copyToClipboardCommands.SubItems.Add( + new ContextMenuItemInfo( + null, + "Session Name & Cursor Time", + this.Navigator.CopyToClipboardCommand, + isEnabled: VisualizationContext.Instance.DatasetViewModel?.CurrentSessionViewModel != null, + commandParameter: VisualizationContext.Instance.DatasetViewModel.CurrentSessionViewModel.Name.ToString() + "@" + this.Navigator.Cursor.ToString("M/d/yyyy HH:mm:ss.ffff"))); + + commands.Add(copyToClipboardCommands); + + commands.Add( + new ContextMenuItemInfo( + null, + $"Go To Time ...", + this.Container.GoToTimeCommand, + isEnabled: true)); + + return commands; } /// @@ -565,9 +539,9 @@ public virtual List GetDerivedStreamVisualizationObj { var derivedStreamVisualizationObjects = new List(); - foreach (IStreamVisualizationObject visualizationObject in this.VisualizationObjects.Where(vo => vo is IStreamVisualizationObject)) + foreach (var visualizationObject in this.VisualizationObjects.OfType()) { - if (visualizationObject.StreamBinding.IsDerived) + if (visualizationObject.StreamBinding.IsBindingToDerivedStream) { derivedStreamVisualizationObjects.Add(visualizationObject); } @@ -584,10 +558,9 @@ public virtual List GetDerivedStreamVisualizationObj /// The partition name of the instance to unbind, or null to unbind all instances. public void UnbindVisualizationObjectsFromStore(string storeName, string storePath, string partitionName) { - foreach (IStreamVisualizationObject streamVisualizationObject in this.VisualizationObjects) + foreach (var streamVisualizationObject in this.VisualizationObjects.OfType()) { - if (streamVisualizationObject != null - && streamVisualizationObject.StreamSource != null + if (streamVisualizationObject.StreamSource != null && streamVisualizationObject.StreamSource.StoreName == storeName && streamVisualizationObject.StreamSource.StorePath == storePath && (partitionName == null || streamVisualizationObject.StreamBinding.PartitionName == partitionName)) @@ -603,17 +576,14 @@ public void UnbindVisualizationObjectsFromStore(string storeName, string storePa /// The currently active session view model. public virtual void UpdateStreamSources(SessionViewModel sessionViewModel) { - foreach (IStreamVisualizationObject streamVisualizationObject in this.VisualizationObjects) + foreach (var streamVisualizationObject in this.VisualizationObjects.OfType()) { streamVisualizationObject?.UpdateStreamSource(sessionViewModel); } } /// - public override string ToString() - { - return this.Name; - } + public override string ToString() => this.Name; /// /// Initializes a new visualization panel. Called by ctor and contract serializer. @@ -641,7 +611,7 @@ protected virtual void OnVisualizationObjectsCollectionChanged(object sender, No { if (e.OldItems != null) { - foreach (VisualizationObject visualizationObject in e.OldItems) + foreach (var visualizationObject in e.OldItems) { if (visualizationObject is IXValueRangeProvider xValueRangeProvider) { @@ -657,7 +627,7 @@ protected virtual void OnVisualizationObjectsCollectionChanged(object sender, No if (e.NewItems != null) { - foreach (VisualizationObject visualizationObject in e.NewItems) + foreach (var visualizationObject in e.NewItems) { if (visualizationObject is IXValueRangeProvider xValueRangeProvider) { @@ -723,9 +693,8 @@ private void ZoomToPanel() { // Get a list of time intervals for all stream visualization objects in this panel var streamIntervals = new List(); - foreach (VisualizationObject visualizationObject in this.VisualizationObjects) + foreach (var streamVisualizationObject in this.VisualizationObjects.OfType()) { - IStreamVisualizationObject streamVisualizationObject = visualizationObject as IStreamVisualizationObject; if (streamVisualizationObject.StreamExtents != TimeInterval.Empty) { streamIntervals.Add(streamVisualizationObject.StreamExtents); @@ -780,12 +749,9 @@ private void OnDeserialized(StreamingContext context) { // After the panel has been deserialized from the layout file, it most likely will contain // some visualization objects that will need their property changed handlers hooked up. - foreach (VisualizationObject visualizationObject in this.VisualizationObjects) + foreach (var yValueRangeProvider in this.VisualizationObjects.OfType()) { - if (visualizationObject is IYValueRangeProvider yValueRangeProvider) - { - yValueRangeProvider.YValueRangeChanged += this.OnVisualizationObjectYValueRangeChanged; - } + yValueRangeProvider.YValueRangeChanged += this.OnVisualizationObjectYValueRangeChanged; } } } diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationPanels/XYVisualizationPanel.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationPanels/XYVisualizationPanel.cs index 12334289b..44ce393f1 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationPanels/XYVisualizationPanel.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizationPanels/XYVisualizationPanel.cs @@ -189,27 +189,17 @@ public Point MousePosition [Browsable(false)] [IgnoreDataMember] public RelayCommand ViewportLoadedCommand - { - get - { - if (this.viewportLoadedCommand == null) + => this.viewportLoadedCommand ??= new RelayCommand( + e => { - this.viewportLoadedCommand = new RelayCommand( - e => - { - // Event source is the viewport - var viewport = e.Source as FrameworkElement; - - // Initialize the display area - this.viewportWidth = viewport.ActualWidth; - this.viewportHeight = viewport.ActualHeight; - this.ZoomToDisplayArea(); - }); - } + // Event source is the viewport + var viewport = e.Source as FrameworkElement; - return this.viewportLoadedCommand; - } - } + // Initialize the display area + this.viewportWidth = viewport.ActualWidth; + this.viewportHeight = viewport.ActualHeight; + this.ZoomToDisplayArea(); + }); /// /// Gets the mouse wheel command. @@ -217,57 +207,47 @@ public RelayCommand ViewportLoadedCommand [Browsable(false)] [IgnoreDataMember] public RelayCommand MouseWheelCommand - { - get - { - if (this.mouseWheelCommand == null) + => this.mouseWheelCommand ??= new RelayCommand( + e => { - this.mouseWheelCommand = new RelayCommand( - e => + if (e.Delta != 0) + { + // Event source is the items control Grid element + var itemsControl = e.Source as FrameworkElement; + + // Get the current mouse location in the items control + var mouseLocation = Mouse.GetPosition(itemsControl); + + // Get the current X Axis and Y Axis logical dimensions + double xAxisLogicalWidth = this.XAxis.Maximum - this.XAxis.Minimum; + double yAxisLogicalHeight = this.YAxis.Maximum - this.YAxis.Minimum; + + // Zoom in our out if there is a non-zero mouse delta + if (e.Delta > 0) { - if (e.Delta != 0) - { - // Event source is the items control Grid element - var itemsControl = e.Source as FrameworkElement; - - // Get the current mouse location in the items control - var mouseLocation = Mouse.GetPosition(itemsControl); - - // Get the current X Axis and Y Axis logical dimensions - double xAxisLogicalWidth = this.XAxis.Maximum - this.XAxis.Minimum; - double yAxisLogicalHeight = this.YAxis.Maximum - this.YAxis.Minimum; - - // Zoom in our out if there is a non-zero mouse delta - if (e.Delta > 0) - { - xAxisLogicalWidth /= ZoomFactor; - yAxisLogicalHeight /= ZoomFactor; - } - else - { - xAxisLogicalWidth *= ZoomFactor; - yAxisLogicalHeight *= ZoomFactor; - } - - // Calculate the new minimum X and Y logical values of the axes such - // that the mouse will still be above the same point in the 2D image - double xAxisLogicalMinimum = this.MousePosition.X - mouseLocation.X * xAxisLogicalWidth / itemsControl.ActualWidth; - double yAxisLogicalMinimum = this.MousePosition.Y - mouseLocation.Y * yAxisLogicalHeight / itemsControl.ActualHeight; - - // Switch to manual axis compute mode - this.AxisComputeMode = AxisComputeMode.Manual; - - this.XAxis.SetRange(xAxisLogicalMinimum, xAxisLogicalMinimum + xAxisLogicalWidth); - this.YAxis.SetRange(yAxisLogicalMinimum, yAxisLogicalMinimum + yAxisLogicalHeight); - } - - e.Handled = true; - }); - } + xAxisLogicalWidth /= ZoomFactor; + yAxisLogicalHeight /= ZoomFactor; + } + else + { + xAxisLogicalWidth *= ZoomFactor; + yAxisLogicalHeight *= ZoomFactor; + } - return this.mouseWheelCommand; - } - } + // Calculate the new minimum X and Y logical values of the axes such + // that the mouse will still be above the same point in the 2D image + double xAxisLogicalMinimum = this.MousePosition.X - mouseLocation.X * xAxisLogicalWidth / itemsControl.ActualWidth; + double yAxisLogicalMinimum = this.MousePosition.Y - mouseLocation.Y * yAxisLogicalHeight / itemsControl.ActualHeight; + + // Switch to manual axis compute mode + this.AxisComputeMode = AxisComputeMode.Manual; + + this.XAxis.SetRange(xAxisLogicalMinimum, xAxisLogicalMinimum + xAxisLogicalWidth); + this.YAxis.SetRange(yAxisLogicalMinimum, yAxisLogicalMinimum + yAxisLogicalHeight); + } + + e.Handled = true; + }); /// /// Gets the mouse right button down command. @@ -275,21 +255,7 @@ public RelayCommand MouseWheelCommand [Browsable(false)] [IgnoreDataMember] public RelayCommand MouseRightButtonDownCommand - { - get - { - if (this.mouseRightButtonDownCommand == null) - { - this.mouseRightButtonDownCommand = new RelayCommand( - e => - { - this.mouseRButtonDownPosition = Mouse.GetPosition(e.Source as FrameworkElement); - }); - } - - return this.mouseRightButtonDownCommand; - } - } + => this.mouseRightButtonDownCommand ??= new RelayCommand(e => this.mouseRButtonDownPosition = Mouse.GetPosition(e.Source as FrameworkElement)); /// /// Gets the mouse right button up command. @@ -297,27 +263,17 @@ public RelayCommand MouseRightButtonDownCommand [Browsable(false)] [IgnoreDataMember] public RelayCommand MouseRightButtonUpCommand - { - get - { - if (this.mouseRightButtonUpCommand == null) + => this.mouseRightButtonUpCommand ??= new RelayCommand( + e => { - this.mouseRightButtonUpCommand = new RelayCommand( - e => - { - if (this.isDraggingAxes) - { - this.isDraggingAxes = false; - - // Prevent the context menu from displaying - e.Handled = true; - } - }); - } + if (this.isDraggingAxes) + { + this.isDraggingAxes = false; - return this.mouseRightButtonUpCommand; - } - } + // Prevent the context menu from displaying + e.Handled = true; + } + }); /// /// Gets the mouse move command. @@ -325,58 +281,48 @@ public RelayCommand MouseRightButtonUpCommand [Browsable(false)] [IgnoreDataMember] public RelayCommand MouseMoveCommand - { - get - { - if (this.mouseMoveCommand == null) + => this.mouseMoveCommand ??= new RelayCommand( + e => { - this.mouseMoveCommand = new RelayCommand( - e => - { - // Event source is the items control Grid element - var itemsControl = e.Source as FrameworkElement; + // Event source is the items control Grid element + var itemsControl = e.Source as FrameworkElement; - // Get the current mouse position - var newMousePosition = e.GetPosition(itemsControl); + // Get the current mouse position + var newMousePosition = e.GetPosition(itemsControl); - // Get the current scale factor between the axes logical bounds and the items control size. - double scaleX = (this.XAxis.Maximum - this.XAxis.Minimum) / itemsControl.ActualWidth; - double scaleY = (this.YAxis.Maximum - this.YAxis.Minimum) / itemsControl.ActualHeight; + // Get the current scale factor between the axes logical bounds and the items control size. + double scaleX = (this.XAxis.Maximum - this.XAxis.Minimum) / itemsControl.ActualWidth; + double scaleY = (this.YAxis.Maximum - this.YAxis.Minimum) / itemsControl.ActualHeight; - // Set the mouse position in locical/image co-ordinates - this.MousePosition = new Point(newMousePosition.X * scaleX + this.XAxis.Minimum, newMousePosition.Y * scaleY + this.YAxis.Minimum); + // Set the mouse position in locical/image co-ordinates + this.MousePosition = new Point(newMousePosition.X * scaleX + this.XAxis.Minimum, newMousePosition.Y * scaleY + this.YAxis.Minimum); - // If the right mouse button is pressed, the user is attempting a drag - if (e.RightButton == MouseButtonState.Pressed) - { - this.isDraggingAxes = true; + // If the right mouse button is pressed, the user is attempting a drag + if (e.RightButton == MouseButtonState.Pressed) + { + this.isDraggingAxes = true; - // Determine how far the mouse moved in logical/image coordinates - double xDelta = (newMousePosition.X - this.mouseRButtonDownPosition.X) * scaleX; - double yDelta = (newMousePosition.Y - this.mouseRButtonDownPosition.Y) * scaleY; + // Determine how far the mouse moved in logical/image coordinates + double xDelta = (newMousePosition.X - this.mouseRButtonDownPosition.X) * scaleX; + double yDelta = (newMousePosition.Y - this.mouseRButtonDownPosition.Y) * scaleY; - // Switch to auto axis compute mode - this.AxisComputeMode = AxisComputeMode.Manual; + // Switch to auto axis compute mode + this.AxisComputeMode = AxisComputeMode.Manual; - // Move the display area bounds by the same logical distance the mouse moved. - this.XAxis.TranslateRange(-xDelta); - this.YAxis.TranslateRange(-yDelta); + // Move the display area bounds by the same logical distance the mouse moved. + this.XAxis.TranslateRange(-xDelta); + this.YAxis.TranslateRange(-yDelta); - // Remember the current mouse position - this.mouseRButtonDownPosition = newMousePosition; + // Remember the current mouse position + this.mouseRButtonDownPosition = newMousePosition; - e.Handled = true; - } - else - { - this.isDraggingAxes = false; - } - }); - } - - return this.mouseMoveCommand; - } - } + e.Handled = true; + } + else + { + this.isDraggingAxes = false; + } + }); /// /// Gets the mouse enter command. @@ -384,17 +330,7 @@ public RelayCommand MouseMoveCommand [Browsable(false)] [IgnoreDataMember] public RelayCommand MouseEnterCommand - { - get - { - if (this.mouseEnterCommand == null) - { - this.mouseEnterCommand = new RelayCommand(() => this.isDraggingAxes = false); - } - - return this.mouseEnterCommand; - } - } + => this.mouseEnterCommand ??= new RelayCommand(() => this.isDraggingAxes = false); /// /// Gets the mouse leave command. @@ -402,17 +338,7 @@ public RelayCommand MouseEnterCommand [Browsable(false)] [IgnoreDataMember] public RelayCommand MouseLeaveCommand - { - get - { - if (this.mouseLeaveCommand == null) - { - this.mouseLeaveCommand = new RelayCommand(() => this.isDraggingAxes = false); - } - - return this.mouseLeaveCommand; - } - } + => this.mouseLeaveCommand ??= new RelayCommand(() => this.isDraggingAxes = false); /// /// Gets the items control size changed command. @@ -420,17 +346,7 @@ public RelayCommand MouseLeaveCommand [Browsable(false)] [IgnoreDataMember] public RelayCommand ViewportSizeChangedCommand - { - get - { - if (this.viewportSizeChangedCommand == null) - { - this.viewportSizeChangedCommand = new RelayCommand(e => this.OnViewportSizeChanged(e)); - } - - return this.viewportSizeChangedCommand; - } - } + => this.viewportSizeChangedCommand ??= new RelayCommand(e => this.OnViewportSizeChanged(e)); /// /// Gets the set auto axis compute mode command for both the X and Y axes. @@ -438,19 +354,22 @@ public RelayCommand ViewportSizeChangedCommand [Browsable(false)] [IgnoreDataMember] public RelayCommand SetAutoAxisComputeModeCommand + => this.setAutoAxisComputeModeCommand ??= new RelayCommand(() => this.AxisComputeMode = AxisComputeMode.Auto); + + /// + public override List ContextMenuItemsInfo() { - get + var items = new List() { - if (this.setAutoAxisComputeModeCommand == null) - { - this.setAutoAxisComputeModeCommand = new RelayCommand(() => - { - this.AxisComputeMode = AxisComputeMode.Auto; - }); - } - - return this.setAutoAxisComputeModeCommand; - } + new ContextMenuItemInfo( + null, + "Auto-Fit Axes", + this.SetAutoAxisComputeModeCommand, + isEnabled: this.AxisComputeMode == AxisComputeMode.Manual), + }; + + items.AddRange(base.ContextMenuItemsInfo()); + return items; } /// diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizerMetadata.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizerMetadata.cs index 0008740e9..a9d1be30b 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizerMetadata.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/VisualizerMetadata.cs @@ -15,7 +15,7 @@ namespace Microsoft.Psi.Visualization using Microsoft.Psi.Visualization.VisualizationPanels; /// - /// Represents a metadata object for visualizing a stream. + /// Represents a metadata object that describes a stream visualizer. /// public class VisualizerMetadata { @@ -23,7 +23,9 @@ public class VisualizerMetadata Type dataType, Type visualizationObjectType, Type streamAdapterType, + object[] streamAdapterArguments, Type summarizerType, + object[] summarizerArguments, string commandText, string iconSourcePath, VisualizationPanelType visualizationPanelType, @@ -37,7 +39,9 @@ public class VisualizerMetadata this.VisualizationPanelType = visualizationPanelType; this.IconSourcePath = iconSourcePath; this.StreamAdapterType = streamAdapterType; + this.StreamAdapterArguments = streamAdapterArguments; this.SummarizerType = summarizerType; + this.SummarizerArguments = summarizerArguments; this.VisualizationFormatString = visualizationFormatString; this.IsInNewPanel = isInNewPanel; this.IsUniversalVisualizer = isUniversalVisualizer; @@ -69,15 +73,25 @@ public class VisualizerMetadata public string IconSourcePath { get; private set; } /// - /// Gets the stream adapter. + /// Gets the stream adapter type. /// public Type StreamAdapterType { get; private set; } /// - /// Gets the summarizer. + /// Gets the stream adapter arguments. + /// + public object[] StreamAdapterArguments { get; private set; } + + /// + /// Gets the summarizer type. /// public Type SummarizerType { get; private set; } + /// + /// Gets the summarizer arguments. + /// + public object[] SummarizerArguments { get; private set; } + /// /// Gets format for the name of the visualization object. /// @@ -97,11 +111,15 @@ public class VisualizerMetadata /// Creates one or two visualizer metadatas depending on whether a "Visualize in new panel" icon source path was suplied by the visualization object. /// /// The visualization object type. - /// The list of known summarizers. - /// The list of known data adapters. + /// The dictionary of all known summarizers, keyed by type. + /// The list of all known stream adapters. /// The log writer where errors should be written to. /// A list of visualizer metadatas. - public static List Create(Type visualizationObjectType, Dictionary summarizers, List dataAdapters, VisualizationLogWriter logWriter) + public static List Create( + Type visualizationObjectType, + Dictionary summarizers, + List streamAdapters, + VisualizationLogWriter logWriter) { // Get the visualization object attribute var visualizationObjectAttribute = GetVisualizationObjectAttribute(visualizationObjectType, logWriter); @@ -159,21 +177,21 @@ public static List Create(Type visualizationObjectType, Dict // 2) Otherwise, use the visualization object's data type Type dataType = summarizerMetadata != null ? summarizerMetadata.InputType : visualizationObjectDataType; - var metadatas = new List(); + var visualizers = new List(); // Add the visualization metadata using no adapter - Create(metadatas, dataType, visualizationObjectType, visualizationObjectAttribute, visualizationPanelTypeAttribute, null); + Create(visualizers, dataType, visualizationObjectType, visualizationObjectAttribute, visualizationPanelTypeAttribute, null); // Find all the adapters that have an output type that's the same as the visualization object's data type (or summarizer input type) - List usableAdapters = dataAdapters.FindAll(a => dataType == a.OutputType || dataType.IsSubclassOf(a.OutputType)); + var applicableStreamAdapters = streamAdapters.FindAll(a => dataType == a.OutputType || dataType.IsSubclassOf(a.OutputType)); // Add the visualization metadata using each of the compatible adapters - foreach (StreamAdapterMetadata adapterMetadata in usableAdapters) + foreach (var streamAdapter in applicableStreamAdapters) { - Create(metadatas, adapterMetadata.InputType, visualizationObjectType, visualizationObjectAttribute, visualizationPanelTypeAttribute, adapterMetadata); + Create(visualizers, streamAdapter.InputType, visualizationObjectType, visualizationObjectAttribute, visualizationPanelTypeAttribute, streamAdapter); } - return metadatas; + return visualizers; } /// @@ -198,12 +216,12 @@ internal VisualizerMetadata GetCloneWithNewStreamAdapterType(Type streamAdapterT } private static void Create( - List metadatas, + List visualizers, Type dataType, Type visualizationObjectType, VisualizationObjectAttribute visualizationObjectAttribute, VisualizationPanelTypeAttribute visualizationPanelTypeAttribute, - StreamAdapterMetadata adapterMetadata) + StreamAdapterMetadata streamAdapterMetadata) { // First, check for the case where the list of visualizer metadatas already contains a // visualizer with an adapter with the same type signature. If so, we need to generate @@ -229,22 +247,22 @@ static bool HasSameAdapterTypes(Type streamAdapterType, Type otherStreamAdapterT return false; } - var sameAdapterVisualizerMetadatas = metadatas.Where(m => HasSameAdapterTypes(m.StreamAdapterType, adapterMetadata?.AdapterType)); + var sameAdapterVisualizers = visualizers.Where(m => HasSameAdapterTypes(m.StreamAdapterType, streamAdapterMetadata?.AdapterType)); var commandTitle = default(string); var inNewPanelCommandTitle = default(string); // If there are other stream adapters with the same type signature - if (sameAdapterVisualizerMetadatas.Any()) + if (sameAdapterVisualizers.Any()) { // Then elaborate the name of the command to include the name of the stream adapter. - var streamAdapterAttribute = adapterMetadata.AdapterType.GetCustomAttribute(); + var streamAdapterAttribute = streamAdapterMetadata.AdapterType.GetCustomAttribute(); var viaStreamAdapterName = string.IsNullOrEmpty(streamAdapterAttribute.Name) ? " (via unnamed adapter)" : $" (via {streamAdapterAttribute.Name} adapter)"; commandTitle = $"{ContextMenuName.VisualizeAs} {visualizationObjectAttribute.CommandText}{viaStreamAdapterName}"; inNewPanelCommandTitle = $"{ContextMenuName.VisualizeAs} {visualizationObjectAttribute.CommandText} in New Panel{viaStreamAdapterName}"; // Also for all the matching adapters, elaborate the names of the corresponding commands - foreach (var sameAdapterOtherVisualizerMetadata in sameAdapterVisualizerMetadatas) + foreach (var sameAdapterOtherVisualizerMetadata in sameAdapterVisualizers) { var otherStreamAdapterAttribute = sameAdapterOtherVisualizerMetadata.StreamAdapterType.GetCustomAttribute(); var viaOtherStreamAdapterName = string.IsNullOrEmpty(otherStreamAdapterAttribute.Name) ? " (via unnamed adapter)" : $" (via {otherStreamAdapterAttribute.Name} adapter)"; @@ -257,37 +275,43 @@ static bool HasSameAdapterTypes(Type streamAdapterType, Type otherStreamAdapterT } else { - commandTitle = (visualizationObjectAttribute.IsUniversalVisualizer || adapterMetadata == null) ? + commandTitle = (visualizationObjectAttribute.IsUniversalVisualizer || streamAdapterMetadata == null) ? $"{ContextMenuName.Visualize} {visualizationObjectAttribute.CommandText}" : $"{ContextMenuName.VisualizeAs} {visualizationObjectAttribute.CommandText}"; - inNewPanelCommandTitle = (visualizationObjectAttribute.IsUniversalVisualizer || adapterMetadata == null) ? + inNewPanelCommandTitle = (visualizationObjectAttribute.IsUniversalVisualizer || streamAdapterMetadata == null) ? $"{ContextMenuName.Visualize} {visualizationObjectAttribute.CommandText} in New Panel" : $"{ContextMenuName.VisualizeAs} {visualizationObjectAttribute.CommandText} in New Panel"; } - metadatas.Add(new VisualizerMetadata( - dataType, - visualizationObjectType, - adapterMetadata?.AdapterType, - visualizationObjectAttribute.SummarizerType, - commandTitle, - visualizationObjectAttribute.IconSourcePath, - visualizationPanelTypeAttribute.VisualizationPanelType, - visualizationObjectAttribute.VisualizationFormatString, - false, - visualizationObjectAttribute.IsUniversalVisualizer)); - - metadatas.Add(new VisualizerMetadata( - dataType, - visualizationObjectType, - adapterMetadata?.AdapterType, - visualizationObjectAttribute.SummarizerType, - inNewPanelCommandTitle, - visualizationObjectAttribute.NewPanelIconSourcePath, - visualizationPanelTypeAttribute.VisualizationPanelType, - visualizationObjectAttribute.VisualizationFormatString, - true, - visualizationObjectAttribute.IsUniversalVisualizer)); + visualizers.Add( + new VisualizerMetadata( + dataType, + visualizationObjectType, + streamAdapterMetadata?.AdapterType, + new object[] { }, + visualizationObjectAttribute.SummarizerType, + new object[] { }, + commandTitle, + visualizationObjectAttribute.IconSourcePath, + visualizationPanelTypeAttribute.VisualizationPanelType, + visualizationObjectAttribute.VisualizationFormatString, + false, + visualizationObjectAttribute.IsUniversalVisualizer)); + + visualizers.Add( + new VisualizerMetadata( + dataType, + visualizationObjectType, + streamAdapterMetadata?.AdapterType, + new object[] { }, + visualizationObjectAttribute.SummarizerType, + new object[] { }, + inNewPanelCommandTitle, + visualizationObjectAttribute.NewPanelIconSourcePath, + visualizationPanelTypeAttribute.VisualizationPanelType, + visualizationObjectAttribute.VisualizationFormatString, + true, + visualizationObjectAttribute.IsUniversalVisualizer)); } private static VisualizationObjectAttribute GetVisualizationObjectAttribute(Type visualizationObjectType, VisualizationLogWriter logWriter) diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Windows/ConfirmLayoutWindow.xaml b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Windows/ConfirmLayoutWindow.xaml new file mode 100644 index 000000000..ff35d8e6a --- /dev/null +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Windows/ConfirmLayoutWindow.xaml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Windows/ConfirmLayoutWindow.xaml.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Windows/ConfirmLayoutWindow.xaml.cs new file mode 100644 index 000000000..bd960e23b --- /dev/null +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Windows/ConfirmLayoutWindow.xaml.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.Psi.Visualization.Windows +{ + using System.Windows; + + /// + /// Interaction logic for ConfirmLayoutWindow.xaml. + /// + public partial class ConfirmLayoutWindow : Window + { + /// + /// Initializes a new instance of the class. + /// + /// The window owner. + /// The nane of the layout to confirm. + public ConfirmLayoutWindow(Window owner, string layoutName) + { + this.InitializeComponent(); + this.Title = "Layout Script Security Warning"; + this.Warning.Text = $"The layout {layoutName} contains one or more embedded scripts which will be executed on the data when applied. These scripts execute code on your machine to generate derived streams for visualization. This code was not written by Microsoft and has not been verified to be free from bugs, security vulnerabilities or malware. Before continuing you should verify that this layout has come from a trusted source."; + this.WarningQuestion.Text = "Are you sure you want to apply this layout?"; + this.DataContext = this; + this.Owner = owner; + } + + private void OKButton_Click(object sender, RoutedEventArgs e) + { + this.DialogResult = true; + e.Handled = true; + } + } +} diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Windows/CreateAnnotationStreamWindow.xaml.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Windows/CreateAnnotationStreamWindow.xaml.cs index a90ea6c0b..8da04e3ff 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Windows/CreateAnnotationStreamWindow.xaml.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Windows/CreateAnnotationStreamWindow.xaml.cs @@ -23,20 +23,26 @@ public partial class CreateAnnotationStreamWindow : Window, INotifyPropertyChang /// /// Initializes a new instance of the class. /// - /// The list of partitions that the annotation stream may be created in. + /// The session that the annotation stream should be added to. /// The list of available annotation schemas the user may choose from. /// The window that wons this window. - public CreateAnnotationStreamWindow(IEnumerable availablePartitions, List availableAnnotationSchemas, Window owner) + public CreateAnnotationStreamWindow(SessionViewModel session, List availableAnnotationSchemas, Window owner) { this.InitializeComponent(); - this.AvailablePartitions = availablePartitions.Where(p => p.IsPsiPartition && !p.IsLivePartition).ToArray(); + this.AvailablePartitions = session.PartitionViewModels.Where(p => p.IsPsiPartition && !p.IsLivePartition).ToArray(); this.AvailableAnnotationSchemas = availableAnnotationSchemas; + // By default, set the store path to the location of the first available partition + if (this.AvailablePartitions.Any()) + { + this.StorePath = this.AvailablePartitions.First().StorePath; + } + this.Owner = owner; this.DataContext = this; - this.ShowPartitionWarningMessage = this.AvailablePartitions.Count() != availablePartitions.Count(); + this.ShowPartitionWarningMessage = this.AvailablePartitions.Count() != session.PartitionViewModels.Count(); if (this.ShowPartitionWarningMessage) { if (this.AvailablePartitions.Any()) diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Windows/ExportPsiPartitionWindow.xaml.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Windows/ExportPsiPartitionWindow.xaml.cs index 09aeb69e0..3c38fe343 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Windows/ExportPsiPartitionWindow.xaml.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Windows/ExportPsiPartitionWindow.xaml.cs @@ -33,8 +33,8 @@ public ExportPsiPartitionWindow(string initialStoreName, string initialStorePath this.StoreName = initialStoreName; this.StorePath = initialStorePath; - this.StartTimeText = DateTimeFormatHelper.FormatDateTime(initialCropInterval.Left); - this.EndTimeText = DateTimeFormatHelper.FormatDateTime(initialCropInterval.Right); + this.StartTimeText = DateTimeHelper.FormatDateTime(initialCropInterval.Left); + this.EndTimeText = DateTimeHelper.FormatDateTime(initialCropInterval.Right); this.Validate(); } diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Windows/ProgressWindow.xaml b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Windows/ProgressWindow.xaml index c6e21f0b5..1b6e7d9ec 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Windows/ProgressWindow.xaml +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Windows/ProgressWindow.xaml @@ -4,6 +4,7 @@ + + + + diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Windows/ProgressWindow.xaml.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Windows/ProgressWindow.xaml.cs index f94d8947c..8e81d6a76 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Windows/ProgressWindow.xaml.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Windows/ProgressWindow.xaml.cs @@ -18,12 +18,14 @@ public partial class ProgressWindow : Window, INotifyPropertyChanged /// /// The owner of this window. /// The text in the progress window. - public ProgressWindow(Window owner, string progressText) + /// Whether the progress window should display the Cancel button. + public ProgressWindow(Window owner, string progressText, bool showCancelButton = false) { this.InitializeComponent(); this.DataContext = this; this.Owner = owner; + this.ShowCancelButton = showCancelButton; this.ProgressText = progressText; this.Progress = 0d; } @@ -49,5 +51,10 @@ public double Progress this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.Progress))); } } + + /// + /// Gets a value indicating whether the Cancel button should be displayed. + /// + public bool ShowCancelButton { get; } } } \ No newline at end of file diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Windows/RunBatchProcessingTaskWindow.xaml b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Windows/RunBatchProcessingTaskWindow.xaml index f8ab819fe..538ee47b0 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Windows/RunBatchProcessingTaskWindow.xaml +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Windows/RunBatchProcessingTaskWindow.xaml @@ -5,6 +5,7 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:xctk="clr-namespace:Xceed.Wpf.Toolkit.PropertyGrid;assembly=Xceed.Wpf.Toolkit" + xmlns:conv="clr-namespace:Microsoft.Psi.Visualization.Converters" ShowInTaskbar="False" WindowStyle="None" ResizeMode="NoResize" @@ -15,6 +16,10 @@ BorderThickness="1" Background="{StaticResource WindowBackgroundBrush}"> + + + + - - @@ -40,15 +40,12 @@ - - - diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Windows/RunBatchProcessingTaskWindow.xaml.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Windows/RunBatchProcessingTaskWindow.xaml.cs index 2e9123b23..1babbb6a2 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Windows/RunBatchProcessingTaskWindow.xaml.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Windows/RunBatchProcessingTaskWindow.xaml.cs @@ -3,6 +3,7 @@ namespace Microsoft.Psi.Visualization.Windows { + using System.Threading; using System.Windows; /// @@ -10,6 +11,8 @@ namespace Microsoft.Psi.Visualization.Windows /// public partial class RunBatchProcessingTaskWindow : Window { + private CancellationTokenSource cancellationTokenSource = null; + /// /// Initializes a new instance of the class. /// @@ -25,22 +28,31 @@ public RunBatchProcessingTaskWindow(Window owner, RunBatchProcessingTaskWindowVi private RunBatchProcessingTaskWindowViewModel ViewModel => this.DataContext as RunBatchProcessingTaskWindowViewModel; - private void RunButtonClick(object sender, RoutedEventArgs e) + private async void RunButtonClick(object sender, RoutedEventArgs e) { if (this.ViewModel.Configuration.Validate(out string error)) { - this.ViewModel - .RunAsync() - .ContinueWith(_ => Application.Current.Dispatcher.Invoke(() => - { - this.DialogResult = true; - this.Close(); - })); + this.cancellationTokenSource = new (); + + await this.ViewModel + .RunAsync(cancellationToken: this.cancellationTokenSource.Token) + .ContinueWith( + task => Application.Current.Dispatcher.Invoke(() => + { + this.DialogResult = !task.IsCanceled; + this.Close(); + })) + .ContinueWith(_ => this.cancellationTokenSource.Dispose()); } else { new MessageBoxWindow(this, "Invalid Configuration", error, cancelButtonText: null).ShowDialog(); } } + + private void CancelButtonClick(object sender, RoutedEventArgs e) + { + this.cancellationTokenSource?.Cancel(); + } } } \ No newline at end of file diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Windows/RunBatchProcessingTaskWindowViewModel.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Windows/RunBatchProcessingTaskWindowViewModel.cs index 23c19b347..d0f9041c0 100644 --- a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Windows/RunBatchProcessingTaskWindowViewModel.cs +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Windows/RunBatchProcessingTaskWindowViewModel.cs @@ -4,9 +4,13 @@ namespace Microsoft.Psi.Visualization.Windows { using System; + using System.Collections.Generic; + using System.IO; using System.Linq; + using System.Threading; using System.Threading.Tasks; using System.Windows; + using GalaSoft.MvvmLight.CommandWpf; using Microsoft.Psi.Data; using Microsoft.Psi.Visualization.Helpers; using Microsoft.Psi.Visualization.ViewModels; @@ -17,6 +21,9 @@ namespace Microsoft.Psi.Visualization.Windows /// public class RunBatchProcessingTaskWindowViewModel : ObservableObject { + private const string DefaultConfiguration = ""; + private const string ConfigurationExtension = ".btconfig"; + private readonly VisualizationContainer visualizationContainer; private readonly DatasetViewModel datasetViewModel; private readonly SessionViewModel sessionViewModel; @@ -32,6 +39,12 @@ public class RunBatchProcessingTaskWindowViewModel : ObservableObject private string elapsedTime = null; private string estimatedRemainingTime = null; private BatchProcessingTaskConfiguration configuration = null; + private string currentConfiguration; + private List availableConfigurations = new (); + private RelayCommand saveConfigurationCommand; + private RelayCommand saveConfigurationAsCommand; + private RelayCommand resetConfigurationCommand; + private RelayCommand deleteConfigurationCommand; /// /// Initializes a new instance of the class. @@ -47,9 +60,10 @@ public RunBatchProcessingTaskWindowViewModel(VisualizationContainer visualizatio this.Name = batchProcessingTaskMetadata.Name; this.Description = batchProcessingTaskMetadata.Description; this.Target = datasetViewModel.Name; - this.DataSize = TimeSpanFormatHelper.FormatTimeSpanApproximate( + this.DataSize = TimeSpanHelper.FormatTimeSpanApproximate( new TimeSpan(datasetViewModel.SessionViewModels.Sum(svm => svm.OriginatingTimeInterval.Span.Ticks))); - this.Configuration = batchProcessingTaskMetadata.GetDefaultConfiguration(); + this.LoadAvailableConfigurations(); + this.CurrentConfiguration = batchProcessingTaskMetadata.MostRecentlyUsedConfiguration ?? DefaultConfiguration; } /// @@ -66,8 +80,9 @@ public RunBatchProcessingTaskWindowViewModel(VisualizationContainer visualizatio this.Name = batchProcessingTaskMetadata.Name; this.Description = batchProcessingTaskMetadata.Description; this.Target = sessionViewModel.Name; - this.DataSize = TimeSpanFormatHelper.FormatTimeSpanApproximate(sessionViewModel.OriginatingTimeInterval.Span); - this.Configuration = batchProcessingTaskMetadata.GetDefaultConfiguration(); + this.DataSize = TimeSpanHelper.FormatTimeSpanApproximate(sessionViewModel.OriginatingTimeInterval.Span); + this.LoadAvailableConfigurations(); + this.CurrentConfiguration = batchProcessingTaskMetadata.MostRecentlyUsedConfiguration ?? DefaultConfiguration; } /// @@ -151,6 +166,34 @@ public BatchProcessingTaskConfiguration Configuration set => this.Set(nameof(this.Configuration), ref this.configuration, value); } + /// + /// Gets or sets the name of the current configuration. + /// + public string CurrentConfiguration + { + get => this.currentConfiguration; + set + { + this.RaisePropertyChanging(nameof(this.CurrentConfiguration)); + + this.currentConfiguration = value ?? DefaultConfiguration; + this.batchProcessingTaskMetadata.MostRecentlyUsedConfiguration = this.currentConfiguration; + + this.LoadCurrentConfiguration(); + + this.RaisePropertyChanged(nameof(this.CurrentConfiguration)); + } + } + + /// + /// Gets or sets the collection of available configurations. + /// + public List AvailableConfigurations + { + get => this.availableConfigurations; + set => this.Set(nameof(this.AvailableConfigurations), ref this.availableConfigurations, value); + } + /// /// Gets or sets the configuration-time visibility. /// @@ -169,11 +212,47 @@ public Visibility RunningVisibility set => this.Set(nameof(this.RunningVisibility), ref this.runningVisibility, value); } + /// + /// Gets the save configuration command. + /// + public RelayCommand SaveConfigurationCommand + => this.saveConfigurationCommand ??= new RelayCommand( + () => + { + if (this.CurrentConfiguration == DefaultConfiguration) + { + this.SaveConfigurationAs(); + } + else + { + this.SaveConfiguration(); + } + }); + + /// + /// Gets the save configuration as command. + /// + public RelayCommand SaveConfigurationAsCommand + => this.saveConfigurationAsCommand ??= new RelayCommand(() => this.SaveConfigurationAs()); + + /// + /// Gets the reset configuration command. + /// + public RelayCommand ResetConfigurationCommand + => this.resetConfigurationCommand ??= new RelayCommand(() => this.ResetConfiguration()); + + /// + /// Gets the delete configuration command. + /// + public RelayCommand DeleteConfigurationCommand + => this.deleteConfigurationCommand ??= new RelayCommand(() => this.DeleteConfiguration(), () => this.CurrentConfiguration != DefaultConfiguration); + /// /// Run the batch processing task. /// + /// An optional token for canceling the asynchronous task. /// The async threading task that runs the batch processing task. - public async Task RunAsync() + public async Task RunAsync(CancellationToken cancellationToken = default) { this.ConfigVisibility = Visibility.Collapsed; this.RunningVisibility = Visibility.Visible; @@ -198,8 +277,8 @@ public async Task RunAsync() this.PercentageCompleteAsString = $"{this.Progress:0.0}%"; var elapsedTime = DateTime.UtcNow - startTime; var estimatedRemainingTime = TimeSpan.FromTicks((long)(elapsedTime.Ticks * ((1 - tuple.Item2) / tuple.Item2))); - this.ElapsedTime = TimeSpanFormatHelper.FormatTimeSpanApproximate(elapsedTime); - this.EstimatedRemainingTime = "about " + TimeSpanFormatHelper.FormatTimeSpanApproximate(estimatedRemainingTime); + this.ElapsedTime = TimeSpanHelper.FormatTimeSpanApproximate(elapsedTime); + this.EstimatedRemainingTime = "about " + TimeSpanHelper.FormatTimeSpanApproximate(estimatedRemainingTime); }); await this.datasetViewModel.Dataset.CreateDerivedPartitionAsync( @@ -211,7 +290,8 @@ public async Task RunAsync() replayDescriptor: this.Configuration.ReplayAllRealTime ? ReplayDescriptor.ReplayAllRealTime : ReplayDescriptor.ReplayAll, deliveryPolicy: this.Configuration.DeliveryPolicyLatestMessage ? DeliveryPolicy.LatestMessage : null, enableDiagnostics: this.Configuration.EnableDiagnostics, - progress: progress); + progress: progress, + cancellationToken: cancellationToken); } else { @@ -227,8 +307,8 @@ public async Task RunAsync() this.PercentageCompleteAsString = $"{this.Progress:0.0}%"; var elapsedTime = DateTime.UtcNow - startTime; var estimatedRemainingTime = TimeSpan.FromTicks((long)(elapsedTime.Ticks * ((1 - tuple.Item2) / tuple.Item2))); - this.ElapsedTime = TimeSpanFormatHelper.FormatTimeSpanApproximate(elapsedTime); - this.EstimatedRemainingTime = "about " + TimeSpanFormatHelper.FormatTimeSpanApproximate(estimatedRemainingTime); + this.ElapsedTime = TimeSpanHelper.FormatTimeSpanApproximate(elapsedTime); + this.EstimatedRemainingTime = "about " + TimeSpanHelper.FormatTimeSpanApproximate(estimatedRemainingTime); }); await this.sessionViewModel.Session.CreateDerivedPartitionAsync( @@ -240,8 +320,195 @@ public async Task RunAsync() replayDescriptor: this.Configuration.ReplayAllRealTime ? ReplayDescriptor.ReplayAllRealTime : ReplayDescriptor.ReplayAll, deliveryPolicy: this.Configuration.DeliveryPolicyLatestMessage ? DeliveryPolicy.LatestMessage : null, enableDiagnostics: this.Configuration.EnableDiagnostics, - progress: progress); + progress: progress, + cancellationToken: cancellationToken); + } + } + + /// + /// Loads the list of available configurations. + /// + private void LoadAvailableConfigurations() + { + // Create a new collection of configurations + var configurations = new List { DefaultConfiguration }; + + // Find all the configuration files and add them to the list of available configurations + var directoryInfo = this.EnsureDirectoryExists(this.batchProcessingTaskMetadata.ConfigurationsPath); + var configurationFiles = directoryInfo.GetFiles($"*{ConfigurationExtension}"); + foreach (FileInfo fileInfo in configurationFiles) + { + string configurationName = Path.GetFileNameWithoutExtension(fileInfo.FullName); + configurations.Add(configurationName); } + + // Set the list of available configurations + this.AvailableConfigurations = configurations; + } + + /// + /// Loads the currently selected configuration. + /// + private void LoadCurrentConfiguration() + { + if (this.CurrentConfiguration == DefaultConfiguration) + { + // Reset the configuration to the default values + this.Configuration = this.batchProcessingTaskMetadata.GetDefaultConfiguration(); + } + else + { + // Attempt to load the configuration from file + string savedConfigurationFile = Path.Combine( + this.batchProcessingTaskMetadata.ConfigurationsPath, + this.CurrentConfiguration + ConfigurationExtension); + + try + { + this.Configuration = BatchProcessingTaskConfiguration.Load(savedConfigurationFile); + } + catch (Exception e) + { + _ = new MessageBoxWindow( + Application.Current.MainWindow, + "Error loading batch task configuration", + "An error occurred while attempting to load the batch task configuration. The default configuration will be used instead.\r\n\r\n" + e.Message, + cancelButtonText: null).ShowDialog(); + + // If the load failed, revert to using the default configuration. This method + // may have been called by the CurrentConfiguration property setter to load + // the selected configuration, so we need to asynchronously dispatch a message + // to change its value back to the default rather than set it directly here. + Application.Current?.Dispatcher.InvokeAsync(() => this.CurrentConfiguration = DefaultConfiguration); + } + } + } + + /// + /// Saves the current configuration. + /// + private void SaveConfiguration() + { + if (this.CurrentConfiguration == DefaultConfiguration) + { + this.SaveConfigurationAs(); + } + else + { + try + { + this.EnsureDirectoryExists(this.batchProcessingTaskMetadata.ConfigurationsPath); + + string fileName = Path.Combine( + this.batchProcessingTaskMetadata.ConfigurationsPath, + this.CurrentConfiguration + ConfigurationExtension); + + this.Configuration.Save(fileName); + } + catch (Exception e) + { + _ = new MessageBoxWindow( + Application.Current.MainWindow, + "Error saving batch task configuration", + "An error occurred while saving the batch task configuration:\r\n\r\n" + e.Message, + cancelButtonText: null).ShowDialog(); + } + } + } + + /// + /// Saves the current configuration as a new named configuration. + /// + private void SaveConfigurationAs() + { + var configurationNameWindow = new GetParameterWindow( + Application.Current.MainWindow, + "Save Configuration As...", + "Configuration Name", + string.Empty); + + bool? result = configurationNameWindow.ShowDialog(); + if (result == true) + { + string configurationName = configurationNameWindow.ParameterValue; + + try + { + this.EnsureDirectoryExists(this.batchProcessingTaskMetadata.ConfigurationsPath); + + string fileName = Path.Combine( + this.batchProcessingTaskMetadata.ConfigurationsPath, + configurationName + ConfigurationExtension); + + // Save the configuration + this.Configuration.Save(fileName); + + // Recreate the configuration list + this.LoadAvailableConfigurations(); + + // Set the current configuration + this.CurrentConfiguration = this.AvailableConfigurations.First(c => c == configurationName); + } + catch (Exception e) + { + _ = new MessageBoxWindow( + Application.Current.MainWindow, + "Error saving batch task configuration", + "An error occurred while saving the batch task configuration:\r\n\r\n" + e.Message, + cancelButtonText: null).ShowDialog(); + } + } + } + + /// + /// Resets the current configuration to the default values. + /// + private void ResetConfiguration() + { + this.Configuration = this.batchProcessingTaskMetadata.GetDefaultConfiguration(); + } + + /// + /// Deletes the current configuration. + /// + private void DeleteConfiguration() + { + var result = new MessageBoxWindow( + Application.Current.MainWindow, + "Are you sure?", + $"Are you sure you want to delete the batch task configuration named \"{this.CurrentConfiguration}\"? This will permanently delete it from disk.", + "Yes", + "Cancel").ShowDialog(); + + if (result == true) + { + string configurationName = this.CurrentConfiguration; + this.CurrentConfiguration = DefaultConfiguration; + + string fileName = Path.Combine( + this.batchProcessingTaskMetadata.ConfigurationsPath, + configurationName + ConfigurationExtension); + + File.Delete(fileName); + this.LoadAvailableConfigurations(); + this.CurrentConfiguration = DefaultConfiguration; + } + } + + /// + /// Ensures that the directory specified by the path exists, creating it if necessary. + /// + /// The path to the directory. + /// A object representing the directory. + private DirectoryInfo EnsureDirectoryExists(string directoryPath) + { + var directoryInfo = new DirectoryInfo(this.batchProcessingTaskMetadata.ConfigurationsPath); + if (!directoryInfo.Exists) + { + directoryInfo.Create(); + } + + return directoryInfo; } } } diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Windows/ScriptWindow.xaml b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Windows/ScriptWindow.xaml new file mode 100644 index 000000000..8a1dc7093 --- /dev/null +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Windows/ScriptWindow.xaml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Windows/ScriptWindow.xaml.cs b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Windows/ScriptWindow.xaml.cs new file mode 100644 index 000000000..3778a5473 --- /dev/null +++ b/Sources/Visualization/Microsoft.Psi.Visualization.Windows/Windows/ScriptWindow.xaml.cs @@ -0,0 +1,360 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.Psi.Visualization.Windows +{ + using System; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Collections.ObjectModel; + using System.ComponentModel; + using System.Linq; + using System.Reflection; + using System.Threading.Tasks; + using System.Windows; + using Microsoft.CodeAnalysis; + using Microsoft.CodeAnalysis.CSharp.Scripting; + using Microsoft.CodeAnalysis.Scripting; + using Microsoft.Psi.PsiStudio.TypeSpec; + using Microsoft.Psi.Visualization.DataTypes; + using Microsoft.Psi.Visualization.Helpers; + using Microsoft.Psi.Visualization.ViewModels; + + /// + /// Interaction logic for ScriptWindow.xaml. + /// + public partial class ScriptWindow : Window, INotifyPropertyChanged + { + private readonly StreamTreeNode streamTreeNode = null; + private string returnTypeName = string.Empty; + private Type returnType = null; + private string scriptText = string.Empty; + private string scriptDerivedStreamName = string.Empty; + private bool isValidating = false; + private string errorMessage = string.Empty; + private int selectedUsingIndex = -1; + private Assembly[] loadedAssemblies = null; + + /// + /// Initializes a new instance of the class. + /// + /// The window owner. + /// The stream tree node for the the stream for which to create an initial script (if any). + /// Indicates whether we are creating a new script or editing an existing one (name and return type cannot be changed for existing scripts). + public ScriptWindow(Window owner, StreamTreeNode streamTreeNode, bool isNewScript = true) + { + // Add some initial usings + var initialUsings = new HashSet + { + "System", + streamTreeNode.DataType.Namespace, + }; + + // Add generic type params of the stream type as well + if (streamTreeNode.DataType.IsGenericType) + { + foreach (var typeArg in streamTreeNode.DataType.GenericTypeArguments) + { + initialUsings.Add(typeArg.Namespace); + } + } + + this.Usings = new ObservableCollection(initialUsings); + + this.Owner = owner; + this.streamTreeNode = streamTreeNode; + this.IsNewScript = isNewScript; + this.Title = isNewScript ? "Create New Script For Derived Stream" : "Edit Script"; + + this.InitializeComponent(); + this.DataContext = this; + + int i = 0; + this.scriptDerivedStreamName = "DerivedStream"; + while (this.streamTreeNode.Children.Any(node => node.Name == $"{this.scriptDerivedStreamName}")) + { + this.scriptDerivedStreamName = $"DerivedStream_{++i}"; + } + + // Create some initial script text for the user. + this.ScriptText = "m"; + this.ReturnTypeName = "object"; + } + + /// + /// The event fired when a bound property changes. + /// + public event PropertyChangedEventHandler PropertyChanged; + + /// + /// Gets or sets the collection of usings/imports for the script engine. + /// + public ObservableCollection Usings { get; set; } + + /// + /// Gets the window title. + /// + public string WindowTitle => this.IsNewScript ? "Create Script Derived Stream" : "Modify Script Derived Stream"; + + /// + /// Gets the content for the OK button. + /// + public string OKButtonContent => this.IsNewScript ? "Create" : "Modify"; + + /// + /// Gets a value indicating whether there is an error message. + /// + public bool HasErrorMessage => !string.IsNullOrEmpty(this.ErrorMessage); + + /// + /// Gets or sets the index of the currently selected using. + /// + public int SelectedUsingIndex + { + get { return this.selectedUsingIndex; } + + set + { + this.selectedUsingIndex = value; + this.OnPropertyChanged(nameof(this.SelectedUsingIndex)); + } + } + + /// + /// Gets or sets the text of the user script. + /// + public string ScriptText + { + get { return this.scriptText; } + + set + { + this.scriptText = value; + this.OnPropertyChanged(nameof(this.ScriptText)); + } + } + + /// + /// Gets or sets the name of the script derived stream. + /// + public string ScriptDerivedStreamName + { + get { return this.scriptDerivedStreamName; } + + set + { + this.scriptDerivedStreamName = value; + this.OnPropertyChanged(nameof(this.ScriptDerivedStreamName)); + } + } + + /// + /// Gets or sets the return type name. + /// + public string ReturnTypeName + { + get { return this.returnTypeName; } + + set + { + this.returnTypeName = value; + this.OnPropertyChanged(nameof(this.ReturnTypeName)); + } + } + + /// + /// Gets or sets the return type of the script. + /// + public Type ReturnType + { + get { return this.returnType; } + + set + { + this.returnType = value; + this.ReturnTypeName = TypeSpec.GetCodeFriendlyName(this.returnType); + } + } + + /// + /// Gets a value indicating whether we are currently validating the script. + /// + public bool IsValidating + { + get { return this.isValidating; } + + private set + { + this.isValidating = value; + this.OnPropertyChanged(nameof(this.IsValidating)); + this.OnPropertyChanged(nameof(this.IsNotValidating)); + } + } + + /// + /// Gets a value indicating whether we are currently not validating the script. + /// + public bool IsNotValidating => !this.IsValidating; + + /// + /// Gets a value indicating whether we are creating a new script. + /// + public bool IsNewScript { get; } + + /// + /// Gets the error text generated during the last executio of the script. + /// + public string ErrorMessage + { + get { return this.errorMessage; } + + private set + { + this.errorMessage = value; + this.OnPropertyChanged(nameof(this.ErrorMessage)); + this.OnPropertyChanged(nameof(this.HasErrorMessage)); + } + } + + /// + /// Gets a list of loaded assemblies. + /// + private Assembly[] LoadedAssemblies => this.loadedAssemblies ??= AppDomain.CurrentDomain.GetAssemblies().Where(a => !a.IsDynamic && !string.IsNullOrEmpty(a.Location)).ToArray(); + + /// + /// Gets the script options. + /// + private ScriptOptions ScriptOptions => ScriptOptions.Default.WithReferences(this.LoadedAssemblies).WithImports(this.Usings); + + /// + /// Called when a bound property changes. + /// + /// The name of the property that changed. + protected void OnPropertyChanged(string propertyName) + { + this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + private async Task Validate() + { + return this.ValidateScriptName() && + await this.ValidateReturnType() && + await this.ValidateScript(); + } + + private bool ValidateScriptName() + { + // Only validate script name when creating a new script + if (this.IsNewScript) + { + if (string.IsNullOrWhiteSpace(this.ScriptDerivedStreamName)) + { + this.ErrorMessage = "Please specify a script name."; + return false; + } + else if (this.streamTreeNode.Children.Any(node => node.Name == $"{this.ScriptDerivedStreamName}")) + { + this.ErrorMessage = $"There is already a derived stream named {this.ScriptDerivedStreamName}. Please specify a unique name for the script."; + return false; + } + else if (this.ScriptDerivedStreamName.Contains('.')) + { + this.ErrorMessage = $"The script derived stream name cannot contain the '.' character."; + return false; + } + } + + return true; + } + + private async Task ValidateReturnType() + { + if (string.IsNullOrWhiteSpace(this.ReturnTypeName)) + { + this.ErrorMessage = "Please enter a return type for the script."; + return false; + } + + try + { + return await Task.Run(async () => + { + // Compute the actual script return Type from the user-specified type name + var result = await CSharpScript.RunAsync($"typeof({this.returnTypeName})", this.ScriptOptions); + this.ReturnType = result.ReturnValue; + return true; + }); + } + catch (CompilationErrorException cee) + { + this.ErrorMessage = $"Error evaluating script return type {this.ReturnTypeName}." + + Environment.NewLine + cee.Diagnostics.EnumerableToString(Environment.NewLine); + + return false; + } + } + + private async Task ValidateScript() + { + try + { + // Create and compile the script to validate it + return await Task.Run(() => + { + // Create the generic method CSharpScript.Create(string, ...) + var createScriptMethod = typeof(CSharpScript).GetMethods() + .Where(m => m.Name == nameof(CSharpScript.Create)) + .FirstOrDefault(m => m.IsGenericMethod && m.GetParameters()[0].ParameterType == typeof(string)) + .MakeGenericMethod(this.ReturnType); + + var globalsType = typeof(ScriptGlobals<>).MakeGenericType(this.streamTreeNode.DataType); + dynamic script = createScriptMethod.Invoke(null, new object[] { this.ScriptText, this.ScriptOptions, globalsType, null }); + + ImmutableArray diagnostics = script.Compile(); + + this.ErrorMessage = diagnostics.EnumerableToString(Environment.NewLine); + return diagnostics.IsEmpty; + }); + } + catch (CompilationErrorException cee) + { + // Errors that occurred before the call to script.Compile + this.ErrorMessage = cee.Diagnostics.EnumerableToString(Environment.NewLine); + return false; + } + } + + private async void OKButton_Click(object sender, RoutedEventArgs e) + { + // Clear any old validation errors and re-validate + this.ErrorMessage = string.Empty; + this.IsValidating = true; + + if (await this.Validate()) + { + // No errors so we can indicate a successful DialogResult + this.DialogResult = true; + e.Handled = true; + } + + this.IsValidating = false; + } + + private void AddUsingButton_Click(object sender, RoutedEventArgs e) + { + var addUsingDialog = new GetParameterWindow(this, "Add Using Clause", "Using", string.Empty); + if (addUsingDialog.ShowDialog() == true && !string.IsNullOrWhiteSpace(addUsingDialog.ParameterValue)) + { + this.Usings.Add(addUsingDialog.ParameterValue); + } + } + + private void RemoveUsingButton_Click(object sender, RoutedEventArgs e) + { + if (this.SelectedUsingIndex >= 0 && this.SelectedUsingIndex < this.Usings.Count) + { + this.Usings.RemoveAt(this.SelectedUsingIndex); + } + } + } +} diff --git a/Sources/Visualization/Test.Psi.Visualization/Properties/AssemblyInfo.cs b/Sources/Visualization/Test.Psi.Visualization/Properties/AssemblyInfo.cs index 46ed7e122..970d114e3 100644 --- a/Sources/Visualization/Test.Psi.Visualization/Properties/AssemblyInfo.cs +++ b/Sources/Visualization/Test.Psi.Visualization/Properties/AssemblyInfo.cs @@ -10,6 +10,6 @@ [assembly: AssemblyCopyright("Copyright (C) Microsoft Corporation. All rights reserved.")] [assembly: ComVisible(false)] [assembly: Guid("7cd463a8-61bb-4937-aa96-02da13e622d0")] -[assembly: AssemblyVersion("0.17.52.1")] -[assembly: AssemblyFileVersion("0.17.52.1")] -[assembly: AssemblyInformationalVersion("0.17.52.1-beta")] +[assembly: AssemblyVersion("0.18.72.1")] +[assembly: AssemblyFileVersion("0.18.72.1")] +[assembly: AssemblyInformationalVersion("0.18.72.1-beta")] diff --git a/build.sh b/build.sh index 9f4af56a3..c709a5fc8 100755 --- a/build.sh +++ b/build.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -(cd ./Sources/Media/Microsoft.Psi.Media.Native.x64/ && . ./build.sh) +(cd ./Sources/Media/Microsoft.Psi.Media_Interop.Linux/ && . ./build.sh) (cd ./Sources/Audio/Microsoft.Psi.Audio/ && . ./build.sh) (cd ./Sources/Audio/Microsoft.Psi.Audio.Linux/ && . ./build.sh) (cd ./Sources/Common/Test.Psi.Common/ && . ./build.sh)