From ab4fee9baf89b7e0afe7842f723466b77bef6d64 Mon Sep 17 00:00:00 2001 From: cjsha Date: Tue, 15 Apr 2025 16:33:24 -0400 Subject: [PATCH 01/17] Add operator to spatial transform ts4231 data --- .../SpatialTransformMatrixDialog.Designer.cs | 459 +++++ .../SpatialTransformMatrixDialog.cs | 233 +++ .../SpatialTransformMatrixDialog.resx | 1784 +++++++++++++++++ .../SpatialTransformMatrixEditor.cs | 54 + OpenEphys.Onix1/SpatialTransform.cs | 32 + 5 files changed, 2562 insertions(+) create mode 100644 OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.Designer.cs create mode 100644 OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs create mode 100644 OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.resx create mode 100644 OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs create mode 100644 OpenEphys.Onix1/SpatialTransform.cs diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.Designer.cs b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.Designer.cs new file mode 100644 index 00000000..2eafd34d --- /dev/null +++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.Designer.cs @@ -0,0 +1,459 @@ +using Bonsai.Design; + +namespace OpenEphys.Onix1.Design +{ + partial class SpatialTransformMatrixDialog + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(SpatialTransformMatrixDialog)); + this.tableLayoutPanelMain = new System.Windows.Forms.TableLayoutPanel(); + this.groupBoxStatus = new System.Windows.Forms.GroupBox(); + this.textBoxStatus = new System.Windows.Forms.TextBox(); + this.labelInstructions = new System.Windows.Forms.Label(); + this.tableLayoutPanelCoordinates = new System.Windows.Forms.TableLayoutPanel(); + this.textBoxUserCoordinate3 = new System.Windows.Forms.TextBox(); + this.textBoxUserCoordinate2 = new System.Windows.Forms.TextBox(); + this.textBoxUserCoordinate1 = new System.Windows.Forms.TextBox(); + this.textBoxUserCoordinate0 = new System.Windows.Forms.TextBox(); + this.textBoxTS4231Coordinate3 = new System.Windows.Forms.TextBox(); + this.textBoxTS4231Coordinate2 = new System.Windows.Forms.TextBox(); + this.textBoxTS4231Coordinate1 = new System.Windows.Forms.TextBox(); + this.buttonMeasure3 = new System.Windows.Forms.Button(); + this.buttonMeasure2 = new System.Windows.Forms.Button(); + this.buttonMeasure1 = new System.Windows.Forms.Button(); + this.labelCoordinate3 = new System.Windows.Forms.Label(); + this.labelCoordinate2 = new System.Windows.Forms.Label(); + this.labelCoordinate1 = new System.Windows.Forms.Label(); + this.buttonMeasure0 = new System.Windows.Forms.Button(); + this.labelHeaderTS4231 = new System.Windows.Forms.Label(); + this.labelCoordinate0 = new System.Windows.Forms.Label(); + this.textBoxTS4231Coordinate0 = new System.Windows.Forms.TextBox(); + this.labelHeaderUser = new System.Windows.Forms.Label(); + this.buttonCalculate = new System.Windows.Forms.Button(); + this.flowLayoutPanelBottom = new System.Windows.Forms.FlowLayoutPanel(); + this.buttonClose = new System.Windows.Forms.Button(); + this.checkBoxApplySpatialTransform = new System.Windows.Forms.CheckBox(); + this.tableLayoutPanelMain.SuspendLayout(); + this.groupBoxStatus.SuspendLayout(); + this.tableLayoutPanelCoordinates.SuspendLayout(); + this.flowLayoutPanelBottom.SuspendLayout(); + this.SuspendLayout(); + // + // tableLayoutPanelMain + // + this.tableLayoutPanelMain.ColumnCount = 1; + this.tableLayoutPanelMain.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); + this.tableLayoutPanelMain.Controls.Add(this.groupBoxStatus, 0, 2); + this.tableLayoutPanelMain.Controls.Add(this.labelInstructions, 0, 0); + this.tableLayoutPanelMain.Controls.Add(this.tableLayoutPanelCoordinates, 0, 1); + this.tableLayoutPanelMain.Controls.Add(this.buttonCalculate, 0, 3); + this.tableLayoutPanelMain.Controls.Add(this.flowLayoutPanelBottom, 0, 4); + this.tableLayoutPanelMain.Dock = System.Windows.Forms.DockStyle.Fill; + this.tableLayoutPanelMain.Location = new System.Drawing.Point(0, 0); + this.tableLayoutPanelMain.Name = "tableLayoutPanelMain"; + this.tableLayoutPanelMain.RowCount = 5; + this.tableLayoutPanelMain.RowStyles.Add(new System.Windows.Forms.RowStyle()); + this.tableLayoutPanelMain.RowStyles.Add(new System.Windows.Forms.RowStyle()); + this.tableLayoutPanelMain.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F)); + this.tableLayoutPanelMain.RowStyles.Add(new System.Windows.Forms.RowStyle()); + this.tableLayoutPanelMain.RowStyles.Add(new System.Windows.Forms.RowStyle()); + this.tableLayoutPanelMain.Size = new System.Drawing.Size(624, 661); + this.tableLayoutPanelMain.TabIndex = 7; + // + // groupBoxStatus + // + this.groupBoxStatus.Controls.Add(this.textBoxStatus); + this.groupBoxStatus.Dock = System.Windows.Forms.DockStyle.Fill; + this.groupBoxStatus.Location = new System.Drawing.Point(3, 263); + this.groupBoxStatus.Name = "groupBoxStatus"; + this.groupBoxStatus.Size = new System.Drawing.Size(618, 330); + this.groupBoxStatus.TabIndex = 6; + this.groupBoxStatus.TabStop = false; + this.groupBoxStatus.Text = "Status Messages"; + // + // textBoxStatus + // + this.textBoxStatus.AcceptsReturn = true; + this.textBoxStatus.AcceptsTab = true; + this.textBoxStatus.Dock = System.Windows.Forms.DockStyle.Fill; + this.textBoxStatus.Location = new System.Drawing.Point(3, 16); + this.textBoxStatus.Multiline = true; + this.textBoxStatus.Name = "textBoxStatus"; + this.textBoxStatus.ReadOnly = true; + this.textBoxStatus.ScrollBars = System.Windows.Forms.ScrollBars.Vertical; + this.textBoxStatus.Size = new System.Drawing.Size(612, 311); + this.textBoxStatus.TabIndex = 3; + this.textBoxStatus.Text = "Awaiting user input...\r\n"; + // + // labelInstructions + // + this.labelInstructions.AutoSize = true; + this.labelInstructions.Dock = System.Windows.Forms.DockStyle.Fill; + this.labelInstructions.Location = new System.Drawing.Point(3, 0); + this.labelInstructions.MaximumSize = new System.Drawing.Size(620, 0); + this.labelInstructions.Name = "labelInstructions"; + this.labelInstructions.Size = new System.Drawing.Size(618, 104); + this.labelInstructions.TabIndex = 4; + this.labelInstructions.Text = resources.GetString("labelInstructions.Text"); + // + // tableLayoutPanelCoordinates + // + this.tableLayoutPanelCoordinates.ColumnCount = 4; + this.tableLayoutPanelCoordinates.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); + this.tableLayoutPanelCoordinates.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); + this.tableLayoutPanelCoordinates.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F)); + this.tableLayoutPanelCoordinates.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F)); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxUserCoordinate3, 3, 4); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxUserCoordinate2, 3, 3); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxUserCoordinate1, 3, 2); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxUserCoordinate0, 3, 1); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxTS4231Coordinate3, 2, 4); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxTS4231Coordinate2, 2, 3); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxTS4231Coordinate1, 2, 2); + this.tableLayoutPanelCoordinates.Controls.Add(this.buttonMeasure3, 1, 4); + this.tableLayoutPanelCoordinates.Controls.Add(this.buttonMeasure2, 1, 3); + this.tableLayoutPanelCoordinates.Controls.Add(this.buttonMeasure1, 1, 2); + this.tableLayoutPanelCoordinates.Controls.Add(this.labelCoordinate3, 0, 4); + this.tableLayoutPanelCoordinates.Controls.Add(this.labelCoordinate2, 0, 3); + this.tableLayoutPanelCoordinates.Controls.Add(this.labelCoordinate1, 0, 2); + this.tableLayoutPanelCoordinates.Controls.Add(this.buttonMeasure0, 1, 1); + this.tableLayoutPanelCoordinates.Controls.Add(this.labelHeaderTS4231, 1, 0); + this.tableLayoutPanelCoordinates.Controls.Add(this.labelCoordinate0, 0, 1); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxTS4231Coordinate0, 2, 1); + this.tableLayoutPanelCoordinates.Controls.Add(this.labelHeaderUser, 3, 0); + this.tableLayoutPanelCoordinates.Dock = System.Windows.Forms.DockStyle.Fill; + this.tableLayoutPanelCoordinates.Location = new System.Drawing.Point(3, 107); + this.tableLayoutPanelCoordinates.Name = "tableLayoutPanelCoordinates"; + this.tableLayoutPanelCoordinates.RowCount = 5; + this.tableLayoutPanelCoordinates.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 30F)); + this.tableLayoutPanelCoordinates.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 30F)); + this.tableLayoutPanelCoordinates.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 30F)); + this.tableLayoutPanelCoordinates.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 30F)); + this.tableLayoutPanelCoordinates.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 30F)); + this.tableLayoutPanelCoordinates.Size = new System.Drawing.Size(618, 150); + this.tableLayoutPanelCoordinates.TabIndex = 5; + // + // textBoxUserCoordinate3 + // + this.textBoxUserCoordinate3.Dock = System.Windows.Forms.DockStyle.Fill; + this.textBoxUserCoordinate3.Location = new System.Drawing.Point(395, 123); + this.textBoxUserCoordinate3.MinimumSize = new System.Drawing.Size(150, 4); + this.textBoxUserCoordinate3.Name = "textBoxUserCoordinate3"; + this.textBoxUserCoordinate3.Size = new System.Drawing.Size(220, 20); + this.textBoxUserCoordinate3.TabIndex = 39; + this.textBoxUserCoordinate3.Text = "x\'3, y\'3, z\'3"; + this.textBoxUserCoordinate3.TextChanged += new System.EventHandler(this.textBoxUserCoordinate3_TextChanged); + // + // textBoxUserCoordinate2 + // + this.textBoxUserCoordinate2.Dock = System.Windows.Forms.DockStyle.Fill; + this.textBoxUserCoordinate2.Location = new System.Drawing.Point(395, 93); + this.textBoxUserCoordinate2.MinimumSize = new System.Drawing.Size(150, 4); + this.textBoxUserCoordinate2.Name = "textBoxUserCoordinate2"; + this.textBoxUserCoordinate2.Size = new System.Drawing.Size(220, 20); + this.textBoxUserCoordinate2.TabIndex = 38; + this.textBoxUserCoordinate2.Text = "x\'2, y\'2, z\'2"; + this.textBoxUserCoordinate2.TextChanged += new System.EventHandler(this.textBoxUserCoordinate2_TextChanged); + // + // textBoxUserCoordinate1 + // + this.textBoxUserCoordinate1.Dock = System.Windows.Forms.DockStyle.Fill; + this.textBoxUserCoordinate1.Location = new System.Drawing.Point(395, 63); + this.textBoxUserCoordinate1.MinimumSize = new System.Drawing.Size(150, 4); + this.textBoxUserCoordinate1.Name = "textBoxUserCoordinate1"; + this.textBoxUserCoordinate1.Size = new System.Drawing.Size(220, 20); + this.textBoxUserCoordinate1.TabIndex = 37; + this.textBoxUserCoordinate1.Text = "x\'1, y\'1, z\'1"; + this.textBoxUserCoordinate1.TextChanged += new System.EventHandler(this.textBoxUserCoordinate1_TextChanged); + // + // textBoxUserCoordinate0 + // + this.textBoxUserCoordinate0.Dock = System.Windows.Forms.DockStyle.Fill; + this.textBoxUserCoordinate0.Location = new System.Drawing.Point(395, 33); + this.textBoxUserCoordinate0.MinimumSize = new System.Drawing.Size(150, 4); + this.textBoxUserCoordinate0.Name = "textBoxUserCoordinate0"; + this.textBoxUserCoordinate0.Size = new System.Drawing.Size(220, 20); + this.textBoxUserCoordinate0.TabIndex = 36; + this.textBoxUserCoordinate0.Text = "x\'0, y\'0, z\'0"; + this.textBoxUserCoordinate0.TextChanged += new System.EventHandler(this.textBoxUserCoordinate0_TextChanged); + // + // textBoxTS4231Coordinate3 + // + this.textBoxTS4231Coordinate3.Dock = System.Windows.Forms.DockStyle.Fill; + this.textBoxTS4231Coordinate3.Location = new System.Drawing.Point(169, 123); + this.textBoxTS4231Coordinate3.MinimumSize = new System.Drawing.Size(150, 4); + this.textBoxTS4231Coordinate3.Name = "textBoxTS4231Coordinate3"; + this.textBoxTS4231Coordinate3.ReadOnly = true; + this.textBoxTS4231Coordinate3.Size = new System.Drawing.Size(220, 20); + this.textBoxTS4231Coordinate3.TabIndex = 33; + this.textBoxTS4231Coordinate3.TabStop = false; + this.textBoxTS4231Coordinate3.Text = "x3, y3, z3"; + this.textBoxTS4231Coordinate3.TextChanged += new System.EventHandler(this.textBoxTS4231Coordinate3_TextChanged); + // + // textBoxTS4231Coordinate2 + // + this.textBoxTS4231Coordinate2.Dock = System.Windows.Forms.DockStyle.Fill; + this.textBoxTS4231Coordinate2.Location = new System.Drawing.Point(169, 93); + this.textBoxTS4231Coordinate2.MinimumSize = new System.Drawing.Size(150, 4); + this.textBoxTS4231Coordinate2.Name = "textBoxTS4231Coordinate2"; + this.textBoxTS4231Coordinate2.ReadOnly = true; + this.textBoxTS4231Coordinate2.Size = new System.Drawing.Size(220, 20); + this.textBoxTS4231Coordinate2.TabIndex = 32; + this.textBoxTS4231Coordinate2.TabStop = false; + this.textBoxTS4231Coordinate2.Text = "x2, y2, z2"; + this.textBoxTS4231Coordinate2.TextChanged += new System.EventHandler(this.textBoxTS4231Coordinate2_TextChanged); + // + // textBoxTS4231Coordinate1 + // + this.textBoxTS4231Coordinate1.Dock = System.Windows.Forms.DockStyle.Fill; + this.textBoxTS4231Coordinate1.Location = new System.Drawing.Point(169, 63); + this.textBoxTS4231Coordinate1.MinimumSize = new System.Drawing.Size(150, 4); + this.textBoxTS4231Coordinate1.Name = "textBoxTS4231Coordinate1"; + this.textBoxTS4231Coordinate1.ReadOnly = true; + this.textBoxTS4231Coordinate1.Size = new System.Drawing.Size(220, 20); + this.textBoxTS4231Coordinate1.TabIndex = 31; + this.textBoxTS4231Coordinate1.TabStop = false; + this.textBoxTS4231Coordinate1.Text = "x1, y1, z1"; + this.textBoxTS4231Coordinate1.TextChanged += new System.EventHandler(this.textBoxTS4231Coordinate1_TextChanged); + // + // buttonMeasure3 + // + this.buttonMeasure3.Location = new System.Drawing.Point(83, 123); + this.buttonMeasure3.Name = "buttonMeasure3"; + this.buttonMeasure3.Size = new System.Drawing.Size(80, 24); + this.buttonMeasure3.TabIndex = 29; + this.buttonMeasure3.Text = "Measure"; + this.buttonMeasure3.UseVisualStyleBackColor = true; + this.buttonMeasure3.Click += new System.EventHandler(this.buttonMeasure3_Click); + // + // buttonMeasure2 + // + this.buttonMeasure2.Location = new System.Drawing.Point(83, 93); + this.buttonMeasure2.Name = "buttonMeasure2"; + this.buttonMeasure2.Size = new System.Drawing.Size(80, 24); + this.buttonMeasure2.TabIndex = 26; + this.buttonMeasure2.Text = "Measure"; + this.buttonMeasure2.UseVisualStyleBackColor = true; + this.buttonMeasure2.Click += new System.EventHandler(this.buttonMeasure2_Click); + // + // buttonMeasure1 + // + this.buttonMeasure1.Anchor = System.Windows.Forms.AnchorStyles.Left; + this.buttonMeasure1.Location = new System.Drawing.Point(83, 63); + this.buttonMeasure1.Name = "buttonMeasure1"; + this.buttonMeasure1.Size = new System.Drawing.Size(80, 24); + this.buttonMeasure1.TabIndex = 23; + this.buttonMeasure1.Text = "Measure"; + this.buttonMeasure1.UseVisualStyleBackColor = true; + this.buttonMeasure1.Click += new System.EventHandler(this.buttonMeasure1_Click); + // + // labelCoordinate3 + // + this.labelCoordinate3.Dock = System.Windows.Forms.DockStyle.Fill; + this.labelCoordinate3.Location = new System.Drawing.Point(3, 120); + this.labelCoordinate3.Name = "labelCoordinate3"; + this.labelCoordinate3.Size = new System.Drawing.Size(74, 30); + this.labelCoordinate3.TabIndex = 18; + this.labelCoordinate3.Text = "Coordinate 3:"; + this.labelCoordinate3.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // labelCoordinate2 + // + this.labelCoordinate2.Dock = System.Windows.Forms.DockStyle.Fill; + this.labelCoordinate2.Location = new System.Drawing.Point(3, 90); + this.labelCoordinate2.Name = "labelCoordinate2"; + this.labelCoordinate2.Size = new System.Drawing.Size(74, 30); + this.labelCoordinate2.TabIndex = 16; + this.labelCoordinate2.Text = "Coordinate 2:"; + this.labelCoordinate2.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // labelCoordinate1 + // + this.labelCoordinate1.Location = new System.Drawing.Point(3, 60); + this.labelCoordinate1.Name = "labelCoordinate1"; + this.labelCoordinate1.Size = new System.Drawing.Size(74, 30); + this.labelCoordinate1.TabIndex = 10; + this.labelCoordinate1.Text = "Coordinate 1:"; + this.labelCoordinate1.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // buttonMeasure0 + // + this.buttonMeasure0.Location = new System.Drawing.Point(83, 33); + this.buttonMeasure0.Name = "buttonMeasure0"; + this.buttonMeasure0.Size = new System.Drawing.Size(80, 24); + this.buttonMeasure0.TabIndex = 1; + this.buttonMeasure0.Text = "Measure"; + this.buttonMeasure0.UseVisualStyleBackColor = true; + this.buttonMeasure0.Click += new System.EventHandler(this.buttonMeasure0_Click); + // + // labelHeaderTS4231 + // + this.tableLayoutPanelCoordinates.SetColumnSpan(this.labelHeaderTS4231, 2); + this.labelHeaderTS4231.Dock = System.Windows.Forms.DockStyle.Fill; + this.labelHeaderTS4231.Location = new System.Drawing.Point(80, 0); + this.labelHeaderTS4231.Margin = new System.Windows.Forms.Padding(0); + this.labelHeaderTS4231.Name = "labelHeaderTS4231"; + this.labelHeaderTS4231.Size = new System.Drawing.Size(312, 30); + this.labelHeaderTS4231.TabIndex = 0; + this.labelHeaderTS4231.Text = "Naive TS4231 Coordinates"; + this.labelHeaderTS4231.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // labelCoordinate0 + // + this.labelCoordinate0.Location = new System.Drawing.Point(3, 30); + this.labelCoordinate0.Name = "labelCoordinate0"; + this.labelCoordinate0.Size = new System.Drawing.Size(74, 30); + this.labelCoordinate0.TabIndex = 2; + this.labelCoordinate0.Text = "Coordinate 0:"; + this.labelCoordinate0.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // textBoxTS4231Coordinate0 + // + this.textBoxTS4231Coordinate0.Dock = System.Windows.Forms.DockStyle.Fill; + this.textBoxTS4231Coordinate0.Location = new System.Drawing.Point(169, 33); + this.textBoxTS4231Coordinate0.MinimumSize = new System.Drawing.Size(150, 4); + this.textBoxTS4231Coordinate0.Name = "textBoxTS4231Coordinate0"; + this.textBoxTS4231Coordinate0.ReadOnly = true; + this.textBoxTS4231Coordinate0.Size = new System.Drawing.Size(220, 20); + this.textBoxTS4231Coordinate0.TabIndex = 30; + this.textBoxTS4231Coordinate0.TabStop = false; + this.textBoxTS4231Coordinate0.Text = "x0, y0, z0"; + this.textBoxTS4231Coordinate0.TextChanged += new System.EventHandler(this.textBoxTS4231Coordinate0_TextChanged); + // + // labelHeaderUser + // + this.labelHeaderUser.Dock = System.Windows.Forms.DockStyle.Fill; + this.labelHeaderUser.Location = new System.Drawing.Point(395, 0); + this.labelHeaderUser.MinimumSize = new System.Drawing.Size(150, 0); + this.labelHeaderUser.Name = "labelHeaderUser"; + this.labelHeaderUser.Size = new System.Drawing.Size(220, 30); + this.labelHeaderUser.TabIndex = 34; + this.labelHeaderUser.Text = "User-Defined Coordinates"; + this.labelHeaderUser.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // buttonCalculate + // + this.buttonCalculate.Dock = System.Windows.Forms.DockStyle.Fill; + this.buttonCalculate.Enabled = false; + this.buttonCalculate.Location = new System.Drawing.Point(3, 599); + this.buttonCalculate.Name = "buttonCalculate"; + this.buttonCalculate.Size = new System.Drawing.Size(618, 23); + this.buttonCalculate.TabIndex = 7; + this.buttonCalculate.Text = "Calculate Spatial Transform"; + this.buttonCalculate.UseVisualStyleBackColor = true; + this.buttonCalculate.Click += new System.EventHandler(this.buttonCalculate_Click); + // + // flowLayoutPanelBottom + // + this.flowLayoutPanelBottom.AutoSize = true; + this.flowLayoutPanelBottom.Controls.Add(this.buttonClose); + this.flowLayoutPanelBottom.Controls.Add(this.checkBoxApplySpatialTransform); + this.flowLayoutPanelBottom.Dock = System.Windows.Forms.DockStyle.Fill; + this.flowLayoutPanelBottom.FlowDirection = System.Windows.Forms.FlowDirection.RightToLeft; + this.flowLayoutPanelBottom.Location = new System.Drawing.Point(3, 628); + this.flowLayoutPanelBottom.Name = "flowLayoutPanelBottom"; + this.flowLayoutPanelBottom.Size = new System.Drawing.Size(618, 30); + this.flowLayoutPanelBottom.TabIndex = 8; + // + // buttonClose + // + this.buttonClose.DialogResult = System.Windows.Forms.DialogResult.OK; + this.buttonClose.Location = new System.Drawing.Point(535, 3); + this.buttonClose.Name = "buttonClose"; + this.buttonClose.Size = new System.Drawing.Size(80, 24); + this.buttonClose.TabIndex = 0; + this.buttonClose.Text = "Close"; + this.buttonClose.UseVisualStyleBackColor = true; + this.buttonClose.Click += new System.EventHandler(this.buttonClose_Click); + // + // checkBoxApplySpatialTransform + // + this.checkBoxApplySpatialTransform.AutoSize = true; + this.checkBoxApplySpatialTransform.Dock = System.Windows.Forms.DockStyle.Fill; + this.checkBoxApplySpatialTransform.Enabled = false; + this.checkBoxApplySpatialTransform.Location = new System.Drawing.Point(268, 3); + this.checkBoxApplySpatialTransform.Name = "checkBoxApplySpatialTransform"; + this.checkBoxApplySpatialTransform.Size = new System.Drawing.Size(261, 24); + this.checkBoxApplySpatialTransform.TabIndex = 1; + this.checkBoxApplySpatialTransform.Text = "Set SpatialTransformMatrix property when closing."; + this.checkBoxApplySpatialTransform.UseVisualStyleBackColor = true; + this.checkBoxApplySpatialTransform.CheckedChanged += new System.EventHandler(this.checkBoxApplySpatialTransform_CheckedChanged); + // + // SpatialTransformMatrixDialog + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(624, 661); + this.Controls.Add(this.tableLayoutPanelMain); + this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); + this.MinimumSize = new System.Drawing.Size(640, 700); + this.Name = "SpatialTransformMatrixDialog"; + this.Text = "TS4231V1 Calibration GUI"; + this.tableLayoutPanelMain.ResumeLayout(false); + this.tableLayoutPanelMain.PerformLayout(); + this.groupBoxStatus.ResumeLayout(false); + this.groupBoxStatus.PerformLayout(); + this.tableLayoutPanelCoordinates.ResumeLayout(false); + this.tableLayoutPanelCoordinates.PerformLayout(); + this.flowLayoutPanelBottom.ResumeLayout(false); + this.flowLayoutPanelBottom.PerformLayout(); + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.TableLayoutPanel tableLayoutPanelMain; + private System.Windows.Forms.GroupBox groupBoxStatus; + private System.Windows.Forms.TextBox textBoxStatus; + private System.Windows.Forms.TableLayoutPanel tableLayoutPanelCoordinates; + private System.Windows.Forms.TextBox textBoxUserCoordinate3; + private System.Windows.Forms.TextBox textBoxUserCoordinate2; + private System.Windows.Forms.TextBox textBoxUserCoordinate1; + private System.Windows.Forms.TextBox textBoxUserCoordinate0; + private System.Windows.Forms.TextBox textBoxTS4231Coordinate3; + private System.Windows.Forms.TextBox textBoxTS4231Coordinate2; + private System.Windows.Forms.TextBox textBoxTS4231Coordinate1; + private System.Windows.Forms.Button buttonMeasure3; + private System.Windows.Forms.Button buttonMeasure2; + private System.Windows.Forms.Button buttonMeasure1; + private System.Windows.Forms.Label labelCoordinate3; + private System.Windows.Forms.Label labelCoordinate2; + private System.Windows.Forms.Label labelCoordinate1; + private System.Windows.Forms.Button buttonMeasure0; + private System.Windows.Forms.Label labelHeaderTS4231; + private System.Windows.Forms.Label labelCoordinate0; + private System.Windows.Forms.TextBox textBoxTS4231Coordinate0; + private System.Windows.Forms.Label labelHeaderUser; + private System.Windows.Forms.Button buttonCalculate; + private System.Windows.Forms.FlowLayoutPanel flowLayoutPanelBottom; + private System.Windows.Forms.Button buttonClose; + private System.Windows.Forms.CheckBox checkBoxApplySpatialTransform; + private System.Windows.Forms.Label labelInstructions; + } +} diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs new file mode 100644 index 00000000..1098ac5d --- /dev/null +++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs @@ -0,0 +1,233 @@ +using System; +using System.Linq; +using System.Numerics; +using System.Windows.Forms; +using System.Reactive.Linq; + +namespace OpenEphys.Onix1.Design +{ + public partial class SpatialTransformMatrixDialog : Form + { + private const byte NumMeasurements = 100; + + private bool[] InputsValid = { false, false, false, false, false, false, false, false }; + + private IObservable> PositionDataSource; + + private Vector3[] TS4231Coordinates = { Vector3.Zero, Vector3.Zero, Vector3.Zero, Vector3.Zero }; + + internal Matrix4x4 SpatialTransform { get; private set; } + + internal bool ApplySpatialTransform { get; private set; } + + internal SpatialTransformMatrixDialog(IObservable> positionDataSource) + { + InitializeComponent(); + PositionDataSource = positionDataSource; + } + + private bool CheckInputValidity(string userInput) + { + string[] serInputSplit = userInput.Split(','); + if (serInputSplit.Length != 3) + { + return false; + } + return serInputSplit.All(floatCandidate => float.TryParse(floatCandidate, out _)); + } + + private void DisableButtons() + { + buttonMeasure0.Enabled = false; + buttonMeasure1.Enabled = false; + buttonMeasure2.Enabled = false; + buttonMeasure3.Enabled = false; + buttonCalculate.Enabled = false; + } + + private void EnableButtons() + { + buttonMeasure0.Invoke((Action)delegate + { + buttonMeasure0.Enabled = true; + }); + buttonMeasure1.Invoke((Action)delegate + { + buttonMeasure1.Enabled = true; + }); + buttonMeasure2.Invoke((Action)delegate + { + buttonMeasure2.Enabled = true; + }); + buttonMeasure3.Invoke((Action)delegate + { + buttonMeasure3.Enabled = true; + }); + buttonCalculate.Invoke((Action)delegate + { + buttonCalculate.Enabled = InputsValid.All(inputValid => inputValid); + }); + } + + private void buttonMeasure_Click(byte coordinate) + { + TextBox[] ts4231TextBoxes = { textBoxTS4231Coordinate0, textBoxTS4231Coordinate1, textBoxTS4231Coordinate2, textBoxTS4231Coordinate3 }; + + textBoxStatus.AppendText(string.Format("Measuring coordinate {0}...", coordinate) + Environment.NewLine); + + DisableButtons(); + + var sharedPositionDataGroups = PositionDataSource.Take(NumMeasurements) + .GroupBy(dataFrame => dataFrame.Item1, dataFrame => dataFrame.Item2) + .Publish(); + + sharedPositionDataGroups + .SelectMany(group => group.Count().Select(count => new { index = group.Key, measurementCount = count })) + .Finally(() => + { + textBoxStatus.Invoke((Action)delegate + { + textBoxStatus.AppendText(string.Format("Measurements at coordinate {0} complete.", coordinate) + + Environment.NewLine + Environment.NewLine + "Awaiting user input..." + Environment.NewLine); + }); + EnableButtons(); + }) + .Subscribe(sensor => + { + textBoxStatus.Invoke((Action)delegate + { + textBoxStatus.AppendText(string.Format("{1} measurements from sensor {0}.", sensor.index, sensor.measurementCount) + Environment.NewLine); + }); + }); + + sharedPositionDataGroups + .Merge() + .Aggregate( + new Vector3(0, 0, 0), + (acc, current) => acc + current, + acc => + { + TS4231Coordinates[coordinate] = acc / NumMeasurements; + ts4231TextBoxes[coordinate].Invoke((Action)delegate + { + ts4231TextBoxes[coordinate].Text = string.Format("{0}, {1}, {2}", + TS4231Coordinates[coordinate].X, + TS4231Coordinates[coordinate].Y, + TS4231Coordinates[coordinate].Z); + }); + return TS4231Coordinates[coordinate]; + }) + .Subscribe(); + + sharedPositionDataGroups.Connect(); + } + + private void textBoxTS4231Coordinate0_TextChanged(object sender, EventArgs e) + { + + buttonCalculate.Enabled = InputsValid.All(inputValid => inputValid); + } + + private void textBoxTS4231Coordinate1_TextChanged(object sender, EventArgs e) + { + buttonCalculate.Enabled = InputsValid.All(inputValid => inputValid); + } + + private void textBoxTS4231Coordinate2_TextChanged(object sender, EventArgs e) + { + buttonCalculate.Enabled = InputsValid.All(inputValid => inputValid); + } + + private void textBoxTS4231Coordinate3_TextChanged(object sender, EventArgs e) + { + buttonCalculate.Enabled = InputsValid.All(inputValid => inputValid); + } + + private void buttonMeasure0_Click(object sender, EventArgs e) + { + buttonMeasure_Click(0); + InputsValid[0] = true; + } + + private void buttonMeasure1_Click(object sender, EventArgs e) + { + buttonMeasure_Click(1); + InputsValid[1] = true; + } + + private void buttonMeasure2_Click(object sender, EventArgs e) + { + buttonMeasure_Click(2); + InputsValid[2] = true; + } + + private void buttonMeasure3_Click(object sender, EventArgs e) + { + buttonMeasure_Click(3); + InputsValid[3] = true; + } + private void textBoxUserCoordinate0_TextChanged(object sender, EventArgs e) + { + InputsValid[4] = CheckInputValidity(textBoxUserCoordinate0.Text); + buttonCalculate.Enabled = InputsValid.All(inputValid => inputValid); + } + + private void textBoxUserCoordinate1_TextChanged(object sender, EventArgs e) + { + InputsValid[5] = CheckInputValidity(textBoxUserCoordinate1.Text); + buttonCalculate.Enabled = InputsValid.All(inputValid => inputValid); + } + + private void textBoxUserCoordinate2_TextChanged(object sender, EventArgs e) + { + InputsValid[6] = CheckInputValidity(textBoxUserCoordinate2.Text); + buttonCalculate.Enabled = InputsValid.All(inputValid => inputValid); + } + + private void textBoxUserCoordinate3_TextChanged(object sender, EventArgs e) + { + InputsValid[7] = CheckInputValidity(textBoxUserCoordinate3.Text); + buttonCalculate.Enabled = InputsValid.All(inputValid => inputValid); + } + + private void buttonCalculate_Click(object sender, EventArgs e) + { + var ts4231V1CoordinatesMatrix = new Matrix4x4( + TS4231Coordinates[0].X, TS4231Coordinates[0].Y, TS4231Coordinates[0].Z, 1, + TS4231Coordinates[1].X, TS4231Coordinates[1].Y, TS4231Coordinates[1].Y, 1, + TS4231Coordinates[2].X, TS4231Coordinates[2].Y, TS4231Coordinates[2].Z, 1, + TS4231Coordinates[3].X, TS4231Coordinates[3].Y, TS4231Coordinates[3].Z, 1); + + float[][] userCoordinates = { + textBoxUserCoordinate0.Text.Split(',').Select(item => float.Parse(item)).ToArray(), + textBoxUserCoordinate1.Text.Split(',').Select(item => float.Parse(item)).ToArray(), + textBoxUserCoordinate2.Text.Split(',').Select(item => float.Parse(item)).ToArray(), + textBoxUserCoordinate3.Text.Split(',').Select(item => float.Parse(item)).ToArray()}; + + var userCoordinatesMatrix = new Matrix4x4( + userCoordinates[0][0], userCoordinates[0][1], userCoordinates[0][2], 1, + userCoordinates[1][0], userCoordinates[1][1], userCoordinates[1][2], 1, + userCoordinates[2][0], userCoordinates[2][1], userCoordinates[2][2], 1, + userCoordinates[3][0], userCoordinates[3][1], userCoordinates[3][2], 1); + + Matrix4x4.Invert(ts4231V1CoordinatesMatrix, out var ts4231V1CoordinatesMatrixInverted); + SpatialTransform = Matrix4x4.Multiply(ts4231V1CoordinatesMatrixInverted, userCoordinatesMatrix); + + textBoxStatus.AppendText("The spatial transform matrix for the above coordinates is:" + Environment.NewLine); + textBoxStatus.AppendText(SpatialTransform.ToString() + Environment.NewLine + Environment.NewLine); + textBoxStatus.AppendText("Awaiting user input..." + Environment.NewLine); + + checkBoxApplySpatialTransform.Enabled = true; + } + + private void buttonClose_Click(object sender, EventArgs e) + { + Close(); + } + + private void checkBoxApplySpatialTransform_CheckedChanged(object sender, EventArgs e) + { + ApplySpatialTransform = checkBoxApplySpatialTransform.Checked; + } + } +} diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.resx b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.resx new file mode 100644 index 00000000..b6de2ecd --- /dev/null +++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.resx @@ -0,0 +1,1784 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Follow the instructions below to transform naive TS4231 position data from a naive reference frame to a user-defined reference frame. For more in-depth instructions, find the corresponding tutorial in Open Ephys' online documentation. +1) Make sure the workflow is running. +2) For each coordinate: + • Place the TS4231V1 device and click the corresponding "Measure" button. + • Input how would like to define the coordinate in the user-defined reference frame. +3) Click the "Calculate Spatial Transform" button which enables after all fields are populated with valid data. +4) To automatically set the SpatialTransformMatrix property, check the bottom checkbox and close this GUI. + + + + + AAABAA4AEBAQAAEABAAoAQAA5gAAABAQAAABAAgAaAUAAA4CAAAQEAAAAQAgAGgEAAB2BwAAICAQAAEA + BADoAgAA3gsAACAgAAABAAgAqAgAAMYOAAAgIAAAAQAgAKgQAABuFwAAMDAQAAEABABoBgAAFigAADAw + AAABAAgAqA4AAH4uAAAwMAAAAQAYAKgcAAAmPQAAMDAAAAEAIACoJQAAzlkAAEBAAAABABgAKDIAAHZ/ + AABAQAAAAQAgAChCAACesQAAAAAAAAEAGABpMQAAxvMAAAAAAAABACAAZ10AAC8lAQAoAAAAEAAAACAA + AAABAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAACAgAAAgICAAACAgADAwMAA//8AAAD/ + /wAAAP8A/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUiAAAAAAAFU5YAAA + AAVVADYgAAAFVQAGYlAAVTUAVTUjMAM1VVVTIDOABVAAAAYDRHAAVQAAYzhlAAAFUAY4AFcAAABVA0AA + NwAAAAUDAAZAAAAAAAAAA0AAAAAAAAACAAAAAAAAAAEAAAAAAAAAAAAA//8AAP/xAAD/wQAA/jEAAPjh + AADDAAAAgBEAAJ+hAADPAwAA5jMAAPJzAAD45wAA/+cAAP/vAAD/7wAA//8AACgAAAAQAAAAIAAAAAEA + CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAI5YzAEWaLgCFpi8AJLioAL2exQA9QTAAO12VANK0 + LwAws5AAzarWAGZsPgA+Z9UAdLRpACbQ+wDdtS8Ak41jAEBr6QDAtkIA37YvAKmdbgA/auoAR27gANKu + MgA8zeMAu5vDAMiq0ABLXUwATaKmAHpvfACWlJUAqqmpALe2tgDGr2QAxKAqAHt6RQA+XowAHJXwALW0 + tACysbEAtLOzALe1swCrjzcAsp5eAK2ecQCQgU8ALk93AA09OADctDEAzqw0AN61LgDbrikA3bMsAMWs + XgC/gxAA1qYjALazrQCMYSEAnGUNAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADg5 + OgAAAAAAAAAAAAAAICA1NjcAAAAAAAAAACAgIAAAMjM0AAAAAAAAICAgAAAAExMwMQAAACYnKCAAACAp + KissLS4vAB0eHyAgICAgISIAIyQlAAAZGgAAAAAAABMAFRscDgAAAAoKAAAAABMUFRYXGAAAAAAACgoA + AA8QEQAAEg4AAAAAAAAKCgALDAAAAA0OAAAAAAAAAAUGBwAAAAgJAAAAAAAAAAAAAAAAAAADBAAAAAAA + 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 + ABMAAAAAAAAAAAAAAAAAAAASAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//////// + //////+H///+B///+AP//8AD//8HA//4HgP/4HwB/wP8ifwP+AHwAAABgAABAYAH5wOH/8YD4/+MB/H/ + kAf4/wEH/H4DD/48Dg//HB4f/4h+H//A/h//4f4///f+P////H////x////8f////P////z///////// + //8oAAAAIAAAAEAAAAABAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACGULgAmmTwAIpQuACGX + NwBNnC4AIZQvACW/wACPqC8AJLmsANS0LwAxly4AI6+JAD5CNwB1oy4AIqNhACbQ+wCchaAALDMkAC02 + KQA8X6cAwbEvACOZPgAmz/cAzarWAMmn0QBmWUwAM0MoADhQRQA+aN0A37YvAFKdLgAlyN8Ano0tAEFS + KwA6V3AAP2rpAJ+qLwAkvLcAxqYuAFliLAA8X6QAP2rqANm1LwA1tJAA2bIvAIF8OAA/Z9AAfLVpAKyb + VgBDbOUAxbZAACbQ+gDCqFEAPszgAEFr5gCrlzoAf8OZADpUWgCAei0AzLE5AIhxjgCkiKsAt5e/AMKn + yQA6WX8AN0wrAEFXUAAsuPcAaVZtAHZpeQCbmZoAsK+vALe2tgBWaGgAPE8rADpt6QAcpPMAdGJ3AIB2 + ggCKiYkAhoWFAIWEhACTkpIAqKenALa1tQCynl4AsI8lALSSJgCxp4oAwLKGAKqRKwBIVSwAPWXVABlb + 5AAUg+0AJMf5ALOysgCrqqoArq2tALazrwCkizoAoIIiAKSMPgC2tLEAoYQpAJl9IgBZZocAIkmpAAY3 + aAALRk0Au5goALaVJwC4pGMAp44+AKKEIgCsnnMAgIqoABAyJAAGLB4A3LMuANayOgC0mSoAmIsqAN60 + LQDftS8A3rQuANOgHwDarSgA2aomANK0VADetS8AyY0RAMyTFgCwlUEAtYMTALJqBQDNlRcAs62dAJdy + HAB9RAEAo2kMAKSQawB6QgEAgUoFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAASY6PkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASUmKi4yNAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAASUlJaIaHiIkeAAAAAAAAAAAAAAAAAAAAAAAAAABJSUlJSUmCg4SFfx4AAAAA + AAAAAAAAAAAAAAAAAABJSUlJSQAAAB58f4CBHgAAAAAAAAAAAAAAAAAAAElJSUlJSQAAAAAeHnx9Hn4e + AAAAAAAAAAAAAAAAAElJSUlJSQAAAAAAHh4eHh4eHh4eAAAAAAAAAAAASUlJSUlJAAAAAAAAAAAeHgAe + eHkAensAAAAAAAAASUlJSUlJAAAAAAAAAAAAb3BxSXJzdHV2dwAAAAAAVWFIYmNVSUlJSUlJSUlJZGVm + Z2hoaWprbG1uAABOT1BRUlNUVUlJSUlJSUlJSUlWV1hZAFpbXF1eX2AAAEVFRkdISUlJSUlJSQAAAAAA + AB4eAAAASkscTE0QAAAAPT4/QAAAAAAAAAAAAAAAAAAeHh4AACpBQkNEEBAAAAAAABgYGAAAAAAAAAAA + AAAAHh4eAAAqKjo7PBAQAAAAAAAAABgYGAAAAAAAAAAAAAAeHgAqKio3OB45EBAAAAAAAAAAABgYGAAA + AAAAAAAAHh41KioqKgAeHjYQEAAAAAAAAAAAABgYGAAAAAAAAB4eMTIqKioAAB4zNBAAAAAAAAAAAAAA + ABgYGAAAAAAeLS4vKioAAAAeHjAQEAAAAAAAAAAAAAAAABgYGAAAACcoKSoqAAAAAB4rLBAAAAAAAAAA + AAAAAAAAABgYGAAhIiMkAAAAAAAAHiUmEAAAAAAAAAAAAAAAAAAAABgZGhscHQAAAAAAAAAeHyAQAAAA + AAAAAAAAAAAAAAAAABESExQAAAAAAAAAABUWFwAAAAAAAAAAAAAAAAAAAAAAAA0AAAAAAAAAAAAADg8Q + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoLDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + CAEJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFBgcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAMEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQIAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///////////// + /4f///4H///4A///wAP//wcD//geA//gfAH/A/yJ/A/4AfAAAAGAAAEBgAfnA4f/xgPj/4wH8f+QB/j/ + AQf8fgMP/jwOD/8cHh//iH4f/8D+H//h/j//9/4////8f////H////x////8/////P///////////ygA + AAAgAAAAQAAAAAEAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsqqXDIxaEWuPWw1DAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC3trYBt7a2Kre2tpCkkGvvekIB/4FKBfHInSsKAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC3trYFt7a2U7e2tru3trb8s62d/5dyHP99RAH/o2kM/9+2 + Lz4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC3trYct7a2fre2tuK3trb/t7a2/7a0sf+wlUH/tYMT/7Jq + Bf/NlRf/37YviAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAALe2tgK3trZBt7a2qbe2tvi3trb/t7a2/7e2tve3tran0rRUx961 + L//JjRH/zJMW/9OgH//fti/SAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAALe2thC3trZst7a207e2tv63trb/t7a2/7e2tt+3trZ7t7a2Gt+2 + L0vfti/93rQt/9OgH//arSj/2aom/9+2L/7fti8gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAALe2tjC3traXt7a28Le2tv+3trb/t7a2/Le2trm3trZPt7a2BQAA + AADfti8Z37Yv59+2L//etC3j37Uv/9+2L/vetC7l37Yv/9+2L2kAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAt7a2CLe2tlm3trbDt7a2/be2tv+3trb/t7a27Le2to63trYnAAAAAAAA + AAAAAAAA37YvA9+2L7bfti//37Yv59+2L4Dfti//37Yv1t+2L5jfti//37YvswAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAt7a2Ibe2toW3trbmt7a2/7e2tv+3trb+t7a2ybe2tmK3trYMAAAAAAAA + AAAAAAAAAAAAAAAAAADfti9x37Yv/9+2L/zfti9L37YvmNyzLv/Wsjq1yrFddrSZKv+YiyryuKAtDAAA + AAAAAAAAAAAAAAAAAAC3trYDt7a2R7e2trC3trb6t7a2/7e2tv+3trb1t7a2oLe2tje3trYCt7a2Are2 + tga3trYUt7a2LLe2tkS3trZcsaR7e7uYKPe2lSf/uKRj9re2ttWnjj73ooQi/6yec/+Aiqj/EDIk/wYs + Hv8iSj1OAAAAAAAAAAC3trYUt7a2cra1tdmzsrL/sK+v/6uqqv+ura3/trW13Le2tpi3trabt7a2s7e2 + tsu3trbht7a29re2tv+3trb/t7a2/7azr/+kizr/oIIi/6SMPv+2tLH/trSx/6GEKf+ZfSL/WWaH/yJJ + qf8GN2j/C0ZN+B1dYCKLeo4HdGJ3n4B2gvOKiYn/hoWF/4WEhP+TkpL/qKen/7a1tf+3trb/t7a2/7e2 + tv+3trb/t7a2/7e2tv+3trb/t7a2/re2tvy3trbwsp5e/LCPJf+0kib0saeKkbe2tnjAsoaDqpEr/0hV + LP89ZdX/GVvk/xSD7f8kx/mvAAAAAI18kDFpVm3/aVZt/3Zpef+bmZr/sK+v/7e2tv23trbwt7a24be2 + tsq3trayt7a2mbe2toK3trZpt7a2Ube2tjm3trYit7a2DN+2L1Tfti/+37Yv/9+2L2YAAAAAP2rqA1Zo + aIk8Tyv/OFBF/zpt6f0cpPP/JtD7/ybQ+0cAAAAAo5OlAohxjoikiKv/t5e//8Knydy7tLw2t7a2Fbe2 + tgm3trYBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADfti8f37Yv69+2L//fti+t37YvAj9q + 6hU/auqyOll//zdMK/9BV1DYLLj3qCbQ+/8m0PvdJtD7AwAAAAAAAAAAAAAAAM2q1nHNqtb8zarW/82q + 1sLNqtYRAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA37YvA9+2L8Dfti//37Yv4d+2 + LxQ/aupCP2rq5D9q6v86VFr/gHot/8yxOYIm0PviJtD7/ybQ+3cAAAAAAAAAAAAAAAAAAAAAAAAAAM2q + 1mDNqtb6zarW/82q1s3NqtYYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADfti9737Yv/9+2 + L/zVsTtFP2rqgD9q6vo/aur/QWvm76uXOv3fti//f8OZnSbQ+/8m0Pv3JtD7GQAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAM2q1lDNqtb1zarW/82q1trNqtYiAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA37YvO9+2 + L/nfti//wqhRoj9q6sA/aur/P2rq/z9q6r+YlIJB37Yv/9+2L/0+zODTJtD7/ybQ+6gAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAM2q1kLNqtbxzarW/82q1uLNqtYsAAAAAAAAAAAAAAAAAAAAAN+2 + Lw/fti/c37Yv/6ybVvtDbOXwP2rq/z9q6vw/auqBP2rqBd+2L1Xfti//xbZA/CbQ+vwm0Pv/JtD7QgAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM2q1jTNqtbozarW/82q1uvNqtY5AAAAAAAA + AADfti8B37YvpNmyL/+BfDj/P2fQ/z9q6v8/aurjP2rqQgAAAAAAAAAA37Yvh9+2L/98tWn/JtD7/ybQ + +9cm0PsCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM2q1inNqtbfzarW/82q + 1vLNqtZFAAAAAN+2L17Gpi7+WWIs/zxfpP8/aur/P2rqtD9q6hYAAAAAAAAAAAAAAADfti+42bUv/zW0 + kP8m0Pv/JtD7cgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM2q + 1h7NqtbVzarW/82q1vjPqaBzno0t8UFSK/86V3D/P2rp+D9q6nI/auoDAAAAAAAAAAAAAAAA37YvAd+2 + L+efqi//JLy3/ybQ+/Qm0PsVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAM2q1hfNqtbJyafR/2ZZTP8zQyj/OFBF/z5o3do/auo2AAAAAAAAAAAAAAAAAAAAAAAA + AADfti8d37Yv/FKdLv8lyN//JtD7owAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAM2q1g6chaC9LDMk/y02Kf48X6emP2rqDwAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAN+2L0zBsS//I5k+/ybP9/4m0Ps8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHdtdgw+QjePP0VDZj9q6gEAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAA37YvfnWjLv8io2H/JtD70ibQ+wEAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADUtC+vMZcu/yOvif8m0PtsAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA37YvAo+oL90hlC7/JLms8ibQ+xIAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADXtS8VTZwu/CGUL/8lv8CcAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGOiNkcilC7/IZc3/iXI + 4DcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANZ5DWyGU + Lv8mmTzQJtD7AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AABSq2EDLJk5UUCjTyIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//////// + /8f///4D///4A///4AP//wAD//wAAf/wBAH/gDgB/gD4APAAAADAAAAAAAAAAQAAAgEAf4ABwf8AA+D/ + AAPwfgAH+DwAB/wYBgf+CA4P/wAcD/+AfB//wPwf/+H8H////D////g////4f///+H////h////4//// + //8oAAAAMAAAAGAAAAABAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAgIAAgIAAAAD/ + AAAA//8AwMDAAICAgAD//wAAgACAAIAAAAD/AP8AAAD/AP///wD/AAAAAACAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAzAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAGOjAAAAAAAAAAAAAAAAAAAAAAAAAAAGZn46MAAAAAAAAAAAAAAAAAAAAA + AAAAZmZzM+YAAAAAAAAAAAAAAAAAAAAAAAZmZ2Yz44cAAAAAAAAAAAAAAAAAAAAABmZ2ZmeHM+iAAAAA + AAAAAAAAAAAAAAAGZ2ZmYHjo44hwAAAAAAAAAAAAAAAAAGZ2ZmZgAIeDaOYwAAAAAAAAAAAAAAAAZmZm + ZgAACDho6HiAAAAAAAAAAAAAAAZnZnZmAAAAaHjoeDh+AAAAAAAAAAAABmZmZmYAAAAAjoeHhweIAAAA + AAAAAABmdmdmYAAAAAADh4CDiAiHAAAAAAAAAGZmZmZgAAAAAACGgwCGhwNzcAAAAAAAZmdmZgAAAAAA + AAh+h2Y+NmcBEAAAAAZ2ZmZnAAAABmZmZnOHNmczd38JAAAABmZmdnZmZmZ2ZmdmZmMzZmc3N8LxIAB3 + d3d3dmZ2Z2ZmdmZnY+M2ZmeDLFzFAAd5d3d2Z2ZmZmZmZmZgY3hwAANwfHVVAAeXmWZmZmZmAAAAAAAA + aIMAAAGRfFVgAAB3ZmAAAAAAAAAAAAAI6HAADHKhxlxQAAAGZmYAAAAAAAAAAACDaAAAxiA3BXVQAAAA + ZrZgAAAAAAAAAABoOADFfHN4BVUAAAAABmZmAAAAAAAAAAjoYAx3x3OHZXUAAAAAAGa2YAAAAAAAAIeD + AMV8UGg2VVAAAAAAAAZttgAAAAAAAIeGfHfHAI5nV1AAAAAAAABmbWAAAAAACDh3x1xwAIeFVQAAAAAA + AAAAZr0AAAAAh4d8V8AACHh1dQAAAAAAAAAABmZgAAAHg3fHfAAAB4NlVQAAAAAAAAAAAGa20AAINyfF + wAAACId1UAAAAAAAAAAAAAZmZgCHonxwAAAACHNXUAAAAAAAAAAAAABmtmcxkscAAAAACDdVAAAAAAAA + AAAAAAAGZnMHJwAAAAAAh2FWAAAAAAAAAAAAAAAAZpApIAAAAAAAgzJVAAAAAAAAAAAAAAAABwGiAAAA + AAAANhdQAAAAAAAAAAAAAAAAABkAAAAAAAAAgxVQAAAAAAAAAAAAAAAAAAAAAAAAAAAAYXUAAAAAAAAA + AAAAAAAAAAAAAAAAAAAIMkUAAAAAAAAAAAAAAAAAAAAAAAAAAAAGMXUAAAAAAAAAAAAAAAAAAAAAAAAA + AAADQiAAAAAAAAAAAAAAAAAAAAAAAAAAAAADI1AAAAAAAAAAAAAAAAAAAAAAAAAAAAABJAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAjEgAAAAAAAAAAAAAAAAAAAAAAAAAAAAABIwAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP// + /////wAA////////AAD//////58AAP/////+HwAA//////APAAD/////wA8AAP////4ADwAA////+AAH + AAD////gEAcAAP///wBwBwAA///8A+AHAAD//+APwAMAAP//gD/AIwAA//wB/4QjAAD/8Af/DCEAAP/A + P/4AAQAA/gD+AAABAAD4AAAAAAEAAMAAAAAAAwAAgAAAEHgDAACAAP/w+AcAAMH//+HgBwAA4P//w8CH + AADwf//DAI8AAPg//4YADwAA/B//DBAfAAD+D/8AMB8AAP8H/gBwPwAA/8P8AeA/AAD/4fgD4D8AAP/w + eAfgfwAA//gwH+B/AAD//AA/4P8AAP/+AP/A/wAA//8B/8D/AAD//4P/wf8AAP//z//B/wAA/////8P/ + AAD/////g/8AAP////+D/wAA/////4f/AAD/////h/8AAP////+P/wAA/////w//AAD/////j/8AAP// + /////wAA////////AAD///////8AACgAAAAwAAAAYAAAAAEACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAIZQuAC2cQQA4nT8AI5xHACSVLgAhlzgAUp0uACGULwAlwMQAlqkvACS4qADXtS8AMZcuACOs + gQAm0PsA37YvAHmkLgAioVoAJtD6AMCwLwAmlS4AJs71AD1BNwA7QDQAVJ0uACXF1wCJd4sALDMkAC44 + LwA8YbUAoqsvACS6rwDNqtYAq5CwAC81KAAvOyYAOFJRAD9p4gDYtS8AN5guACOviQCbfXkAQEMmADZL + KwA3TCsAOll/AD9q6QB9pC4AIqNgAMypzQDHpS8AaGssADhMKwA3TC0APGGyAD9q6gDGsi8AJptCACbN + 8gDbtC8AkoYtADtPKwA4T0AAPmfWAFieMAAlx9wAu58uAFBbLAA5VWIAP2roAKWsLwAlvLcA0q4vAHFx + LQA8XZUA2bUvAD61jwDetS8Am4w1AEVpxACCtmkAwKZJAEpv3QDItj0AJ9D5ANKwPgBAaukA3rYvAELM + 3ACFw5IAP2rmAHV3SADUry8A2bc2ADpWaABTXiwAvaAuADtbjQA9UCsAioEvAIBqhQCKcpAAoISnAK+R + tgC7ocEAPWGxADhNLQA+ZtMAKcP5AHdlewBpVm0AeWp8AKGeoAC0s7MAt7a2ADlOLQA9YrkANHLrAB+y + 9gB2ZHoAcmV0AIKBgQCDgoIAh4aGAJqZmQCura0AyrFhANCpLADUrS0A2bEuANy0LgCymS4ATVorADtc + kgA+auoAElznABiU8QAlzfsAeWh8AIh+iQCRkJAAjYyMAImIiACGhYUAhYSEAISDgwCmpaUAtbS0ALe1 + tACnkU0AoYMiAKKEIgCkhSMArqB3AMCjRQC4mCkAXmEoADdSaQA9ZuIAJl/kAAlV5AARdusAIsH4ALGw + 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 + AAAAAAAAAAAAAAAAAAAAAAAAISEhISEAAAAAEENERUY4ODgAAAAAAAAQEEdIDw8AAAAAAAAAAAAAAAAA + AAAAAAAAACEhISEhAAA8PT4/QDg4AAAAAAAAAAAQEEFCDw8AAAAAAAAAAAAAAAAAAAAAAAAAAAAhISEh + MjM0NTY3ODgAAAAAAAAAAAAQOTo7DwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAISEhKissLS4vAAAAAAAA + AAAAABAQMDEPDwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACEiIxwkJSYAAAAAAAAAAAAAABAnKCkPDwAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbHBwdHgAAAAAAAAAAAAAAABAfASAPAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAFxgAAAAAAAAAAAAAAAAAABAZARoPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAABQVBhYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAEBEBEhMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADA0BDg8AAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgEBCwAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwEICQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAABQEGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAADAQEEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQECAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////////AAD///////8AAP//////nwAA//////4f + AAD/////8A8AAP/////ADwAA/////gAPAAD////4AAcAAP///+AQBwAA////AHAHAAD///wD4AcAAP// + 4A/AAwAA//+AP8AjAAD//AH/hCMAAP/wB/8MIQAA/8A//gABAAD+AP4AAAEAAPgAAAAAAQAAwAAAAAAD + AACAAAAQeAMAAIAA//D4BwAAwf//4eAHAADg///DwIcAAPB//8MAjwAA+D//hgAPAAD8H/8MEB8AAP4P + /wAwHwAA/wf+AHA/AAD/w/wB4D8AAP/h+APgPwAA//B4B+B/AAD/+DAf4H8AAP/8AD/g/wAA//4A/8D/ + AAD//wH/wP8AAP//g//B/wAA///P/8H/AAD/////w/8AAP////+D/wAA/////4P/AAD/////h/8AAP// + //+H/wAA/////4//AAD/////D/8AAP////+P/wAA////////AAD///////8AAP///////wAAKAAAADAA + AABgAAAAAQAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIpWC49bDQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALWyqoVVGHpCAXpCAQAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALe2 + tre2tre2taeTVn9JA3pCAXpCAa58FwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAALe2tre2tre2tre2tq+jgJx6HINPBIBGAZxcA9mrKAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALe2tre2tre2tre2tre2tre2trOun6OHLKh/Ga1v + B6xgA8OCC960LQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALe2tre2tre2tre2 + tre2tre2tre2tri2ssGlSNWuLc6XGMOBCb57CMmOEt+2L9+2LwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAALe2tre2tre2tre2tre2tre2tre2tre2tgAAANy2Ot+2L9+2L8mNEc+ZGs2VF8+YGd+2L9+2 + LwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAALe2tre2tre2tre2tre2tre2tre2tre2tre2tgAAAAAAAAAAAN+2 + L9+2L960LcqPE960LdWkItSiIN+2L9+2LwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALe2tre2tre2tre2tre2tre2tre2 + tre2tgAAAAAAAAAAAAAAAAAAAN+2L9+2L9+2L9uvKtqtKN+2L92yLNqsKN+2L9+2LwAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALe2tre2 + tre2tre2tre2tre2tre2tre2tre2tgAAAAAAAAAAAAAAAAAAAAAAAN+2L9+2L9+2L9+2L9+1L9+2L9+2 + L9+2L9+1Lt+2L9+2L9+2LwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAALe2tre2tre2tre2tre2tre2tre2tre2tre2tgAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAN+2L9+2L9+2L9+2L9+2L9+2L9+2L9+2LwAAAN+2L9+2L9+2LwAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALe2tre2tre2tre2tre2tre2tre2tre2tre2tgAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAN+2L9+2L9+2L9+2LwAAAN+2L9+2L9+2L9+2LwAAAN+2L9+2 + L9+2LwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALe2tre2tre2tre2tre2 + tre2tre2tre2tre2tgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAN+2L9+2L9+2L9+2LwAA + AAAAAN+2L9uyLtevLc6xUAAAAMakNYyBKGhuJp+RKwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAALe2tre2tre2tre2tre2tre2tre2tre2tgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAMWjMsmkKsOfKb2aKL6pZre2trSunKaHJKOFI6KDIrKqlK6vtVJfWgctHgYsHhI4KAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAALe2tre2tre2tre2tre2tre2tre2tre2tre2tgAAAAAAAAAAAAAAAAAA + AAAAAAAAALe2tre2tre2tre2tre2tre2tre2trCni6CCI6CCIqCCIqWOQ7a0sbe2tq+lhqCCIqCCIqGF + KpudpkReqhQ9fwYsHgYsHgw0KQAAAAAAAAAAAAAAAAAAAAAAALe2tre2trSzs7GwsK6tramoqKSjo6al + pbKxsbe2tre2tre2tre2tre2tre2tre2tre2tre2tre2tre2tre2tre2tre2tre2trSwpaKHMaCCIqCC + IqGFK7Svobe2tre2tqubaKCCIp+BIm1lNzJQpypMqghAqQY5bQlDVhl8jAAAAAAAAAAAAHlofIh+iZGQ + kI2MjImIiIaFhYWEhISDg5GQkKalpbW0tLe2tre2tre2tre2tre2tre2tre2tre2tre2tre2tre2tre2 + tre2tre2tre2tre1tKeRTaGDIqKEIqSFI66gd7e2tre2tre2tsCjRbiYKV5hKDdSaT1m4iZf5AlV5BF2 + 6yLB+AAAAAAAAAAAAHZkemlWbWlWbXJldIKBgYOCgoeGhpqZma6trbe2tre2tre2tre2tre2tre2tre2 + tre2tre2tre2tre2tre2tre2tre2tre2tre2tre2tgAAAMqxYdCpLNStLdmxLty0LgAAAAAAAAAAAAAA + ALKZLk1aKzdMKztckj5q6hJc5xiU8SXN+ybQ+wAAAAAAAAAAAHdle2lWbWlWbWlWbXlqfKGeoLSzs7e2 + tre2tre2tre2tre2tre2tre2tre2tgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAN+2 + L9+2L9+2L9+2LwAAAAAAAAAAAAAAAAAAADlOLTdMKzdMKz1iuTRy6x+y9ibQ+ybQ+wAAAAAAAAAAAAAA + AAAAAIBqhYpykKCEp6+RtruhwQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAN+2L9+2L9+2L9+2LwAAAAAAAAAAAAAAAD9q6j1hsTdMKzdMKzhNLT5m + 0ynD+SbQ+ybQ+ybQ+wAAAAAAAAAAAAAAAAAAAAAAAM2q1s2q1s2q1s2q1s2q1gAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAN+2L9+2L9+2L9+2LwAAAAAAAAAA + AAAAAD9q6j9q6jtbjTdMKz1QK4qBLwAAACbQ+ybQ+ybQ+ybQ+wAAAAAAAAAAAAAAAAAAAAAAAAAAAM2q + 1s2q1s2q1s2q1s2q1gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAN+2L9+2L9+2L9+2LwAAAAAAAD9q6j9q6j9q6j9q6jpWaFNeLL2gLt+2LwAAACbQ+ybQ+ybQ+wAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM2q1s2q1s2q1s2q1s2q1gAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAN+2L9+2L9+2L9+2LwAAAAAAAD9q6j9q6j9q6j9q6j9q5nV3 + SNSvL9+2L9m3NibQ+ybQ+ybQ+ybQ+wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM2q1s2q1s2q + 1s2q1s2q1gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAN+2L9+2L9+2L9+2LwAA + AAAAAD9q6j9q6j9q6j9q6j9q6gAAAN62L9+2L9+2L4XDkibQ+ybQ+ybQ+wAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAM2q1s2q1s2q1s2q1s2q1gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAN+2L9+2L9+2L9KwPkBq6T9q6j9q6j9q6j9q6j9q6gAAAAAAAN+2L9+2L962L0LM3CbQ + +ybQ+ybQ+wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM2q1s2q1s2q1s2q1s2q + 1gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAN+2L9+2L9+2L8CmSUpv3T9q6j9q6j9q6j9q6j9q + 6gAAAAAAAAAAAN+2L9+2L8i2PSfQ+SbQ+ybQ+wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAM2q1s2q1s2q1s2q1gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAN+2L9+2L961 + L5uMNUVpxD9q6j9q6j9q6j9q6gAAAAAAAAAAAAAAAN+2L9+2L9+2L4K2aSbQ+ybQ+ybQ+wAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM2q1s2q1s2q1s2q1gAAAAAA + AAAAAAAAAAAAAAAAAN+2L9+2L9KuL3FxLTxdlT9q6j9q6j9q6j9q6gAAAAAAAAAAAAAAAAAAAN+2L9+2 + L9m1Lz61jybQ+ybQ+ybQ+wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAM2q1s2q1s2q1s2q1s2q1gAAAAAAAAAAAAAAAN+2L7ufLlBbLDlVYj9q6D9q6j9q6j9q + 6gAAAAAAAAAAAAAAAAAAAAAAAN+2L9+2L6WsLyW8tybQ+ybQ+wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM2q1s2q1s2q1s2q1s2q1gAAAAAAANu0 + L5KGLTtPKzhPQD5n1j9q6j9q6gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAN+2L9+2L1ieMCXH3CbQ+ybQ + +wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAM2q1s2q1s2q1s2q1sypzcelL2hrLDhMKzdMLTxhsj9q6j9q6gAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAN+2L8ayLyabQibN8ibQ+wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM2q1s2q1s2q1pt9eUBDJjZLKzdMKzpZfz9q6QAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAN+2L9+2L32kLiKjYCbQ+ybQ+wAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM2q + 1quQsC81KCwzJC87JjhSUT9p4gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAN+2L9i1LzeY + LiOviSbQ+ybQ+wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIl3iywzJCwzJC44LzxhtQAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAN+2L6KrLyGULiS6rybQ+wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD1BNztANAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAN+2L1SdLiGULiXF1ybQ+wAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAMCwLyaVLiGXOCbO9QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAN+2L3mkLiGULiKhWibQ+gAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANe1LzGXLiGULiOs + gSbQ+wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAJapLyGULiGULiS4qAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFKdLiGULiGULyXAxAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + ACSVLiGULiGXOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAADidPyGULiGULiOcRwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACGULiGULi2cQQAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///////wAA//////// + AAD//////58AAP/////+HwAA//////APAAD/////wA8AAP////4ADwAA////+AAHAAD////gEAcAAP// + /wBwBwAA///8A+AHAAD//+APwAMAAP//gD/AIwAA//wB/4QjAAD/8Af/DCEAAP/AP/4AAQAA/gD+AAAB + AAD4AAAAAAEAAMAAAAAAAwAAgAAAEHgDAACAAP/w+AcAAMH//+HgBwAA4P//w8CHAADwf//DAI8AAPg/ + /4YADwAA/B//DBAfAAD+D/8AMB8AAP8H/gBwPwAA/8P8AeA/AAD/4fgD4D8AAP/weAfgfwAA//gwH+B/ + AAD//AA/4P8AAP/+AP/A/wAA//8B/8D/AAD//4P/wf8AAP//z//B/wAA/////8P/AAD/////g/8AAP// + //+D/wAA/////4f/AAD/////h/8AAP////+P/wAA/////w//AAD/////j/8AAP///////wAA//////// + AAD///////8AACgAAAAwAAAAYAAAAAEAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + 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 + ogYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC3trYyt7a2mre2tu+3trb/t7a2/7e2tv+3trb/t7a2/7e2 + tvm3traqt7a2Q7e2tgO3trYIt7a2ILe2tji3trZPt7a2aLe2toC3traXt7a2sLe2tsm3trbat7a27Le2 + tvqwp4v/oIIj/6CCIv+ggiL/pY5D/7a0sf+3trb/r6WG/6CCIv+ggiL/oYUq/5udpv9EXqr/FD1//wYs + Hv8GLB7/DDQp7WSTmw0AAAAAAAAAAAAAAAC3trYNt7a2XLe2tsa3trb+tLOz/7GwsP+ura3/qaio/6Sj + o/+mpaX/srGx9Le2tsC3tra/t7a21be2tu+3trb+t7a2/re2tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2 + tv+3trb/t7a2/7Swpf+ihzH/oIIi/6CCIv+hhSv/tK+h/7e2tv+3trb/q5to/6CCIv+fgSL/bWU3/zJQ + p/8qTKr/CECp/wY5bf8JQ1b/GXyMigAAAAAAAAAAkoKVGXlofJOIfonkkZCQ/42MjP+JiIj/hoWF/4WE + hP+Eg4P/kZCQ/6alpf+1tLT/t7a2/7e2tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2 + tv+3trb/t7a2/7e2tv+3trb/t7W0/6eRTf+hgyL/ooQi/6SFI/6uoHfgt7a2xLe2tq63traWwKNF4riY + Kf9eYSj/N1Jp/z1m4v8mX+T/CVXk/xF26/8iwfj5JtD7JgAAAAAAAAAAdmR6sWlWbf9pVm3/cmV0/4KB + gf+DgoL/h4aG/5qZmf+ura3/t7a2/7e2tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2 + tv+3trb5t7a25be2ts23tra3t7a2n7e2toe3trZvyrFhudCpLP/UrS3/2bEu/9y0LqTfti8CAAAAAAAA + AAAAAAAAspku501aK/83TCv/O1yS/z5q6v8SXOf/GJTx/yXN+/8m0Pu4JtD7AgAAAAAAAAAAd2V7xmlW + bf9pVm3/aVZt/3lqfP+hnqD/tLOz/7e2tv+3trb+t7a287e2tua3trbYt7a2wLe2tqm3traQt7a2eLe2 + tmG3trZIt7a2Mbe2thi3trYGt7a2A7e2tgEAAAAAAAAAAAAAAADfti8z37Yv99+2L//fti//37Yv2d+2 + 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 + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAN+2LwHXtS/DMZcu/yGU + Lv8jrIH/JtD7oQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAN+2 + LwyWqS/qIZQu/yGULv8kuKj/JtD7OgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAANW0LydSnS7/IZQu/yGUL/8lwMTOJtD7AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAIClL1kklS7/IZQu/yGXOP8lx9xqAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADidP5QhlC7/IZQu/yOcR/Am0PsUAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADefRX8hlC7/IZQu/y2c + QaEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFKr + YQgxnD9zLZo6fFeuZhQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///////8AAP///////wAA//////4PAAD/////+A8AAP// + ///ADwAA/////wAHAAD////4AAcAAP///+AABwAA////gAAHAAD///wAAAMAAP//8ADAAwAA//+AA4AD + AAD//gAPgAEAAP/4AH8AAQAA/8AD/gABAAD/AA/gAAAAAPwAAAAAAAAA4AAAAAABAACAAAAAAAEAAIAA + AAA4AQAAgAAA4GADAACAB//AwAMAAMB//8GABwAA4D//gAAHAADwH/8AAA8AAPgP/gAADwAA/Af+AAAP + AAD+A/wAIB8AAP8A+ADgHwAA/4BwAcA/AAD/4DADwD8AAP/wAA/APwAA//gAH8B/AAD//AA/wH8AAP/+ + AP/A/wAA//8B/4D/AAD//4P/gP8AAP//z/+B/wAA/////4H/AAD/////A/8AAP////8D/wAA/////wP/ + AAD/////B/8AAP////8H/wAA/////w//AAD/////D/8AAP///////wAA////////AAAoAAAAQAAAAIAA + AAABABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjV0YiVQKi1YLAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAt7a2n4Ve + ekIBekIBekIBh1IIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAt7a2 + t7a2t7a2squWjWEUekIBekIBekIBiVQJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAt7a2t7a2t7a2t7a2trOupIs9i14LekIBekIBekIBpG8PAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAt7a2t7a2t7a2t7a2t7a2t7W1qZddn4EhjF0HfEUBhUgBnloDz5kb37YvAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAt7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2r6SCoIMkoX8eomwIqGMErF8Cv3wI1qUj + 37YvAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAt7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2tK+grpAzw58pzp0gw4IJ + unQGuXMGw4IJ268q37YvAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAt7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2uLaz0LBH + 3bQv37Yv0Joaw4IJyY0SxIMKxocN3rQu37Yv37YvAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAt7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2 + t7a2AAAAAAAA37Yv37Yv37UvypATx4kO3LAryY4Ry5AT37Yv37Yv37YvAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAt7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2 + t7a2t7a2AAAAAAAAAAAAAAAA37Yv37Yv37Yv3rQtxogO2Kgl37Yv0Zwc0Joa37Yv37Yv37YvAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAt7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2 + t7a2t7a2t7a2t7a2AAAAAAAAAAAAAAAAAAAA37Yv37Yv37Yv37Yv27Aq0Jsb37Yv37Yv2asn1aMh37Yv + 37Yv37YvAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAt7a2t7a2t7a2t7a2t7a2t7a2 + t7a2t7a2t7a2t7a2t7a2AAAAAAAAAAAAAAAAAAAAAAAAAAAA37Yv37Yv37Yv37Yv37Yv264p3rQu37Yv + 37Yv3rUu268q37Yv37Yv37Yv37YvAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAt7a2t7a2t7a2t7a2t7a2 + t7a2t7a2t7a2t7a2t7a2t7a2t7a2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA37Yv37Yv37Yv37Yv + 37Yv37Yv37Yv37Yv37Yv37Yv37Yv37Yv37Yv37Yv37YvAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAt7a2t7a2t7a2 + t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA37Yv + 37Yv37Yv37Yv37YvAAAA37Yv37Yv37Yv37Yv37YvAAAA37Yv37Yv37Yv37YvAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAt7a2 + t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAA37Yv37Yv37Yv37Yv37Yv37YvAAAA37Yv37Yv37Yv37Yv37YvAAAA37Yv37Yv37Yv37Yv37Yv + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAA37Yv37Yv37Yv37Yv37Yv37YvAAAAAAAA37Yv37Yv37Yv37YvAAAAAAAA37Yv + 37Yv37Yv37Yv37YvAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAt7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2AAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA37Yv37Yv37Yv37Yv37YvAAAAAAAAAAAA3rUv2rIu1q8t + 0qwuAAAAAAAAv6hesJMnYWclOlIjdXcnAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAt7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2AAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA168t1a4t0KosyqUqxKAqw61ot7a2t7a2 + r51kp4gjpYYjo4Ujo4gvtbGpt7a2l5iXLEAjBiweBiweBy0eAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAt7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2AAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAt7a2t7a2t7a2t7WzqZNLpIUjoIIioIIioIIi + ppBKtrSwt7a2t7a2qJNToIIioIIioIIipIw+tbS1gIywMlCkCjAzBiweBiweBiweHkc/AAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAt7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2AAAAAAAA + AAAAAAAAAAAAAAAAt7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2rqF6oIIi + oIIioIIioIIioocvtK+it7a2t7a2t7W0o4k2oIIioIIioIIino5aXXGsLUynHkamBjRgBiweBiweBi0g + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAt7a2t7a2t7a2tLOzsrGxrq2tq6qqp6amoqGhnJubnp2dq6qq + tLOzt7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2 + t7a2sqyYoYUroIIioIIioIIioIIjr6WFt7a2t7a2t7a2tLCkoYUqoIIioIIigm8hPE5uLUynLk6rD0Os + BkGrBzt4CEBgF4SdAAAAAAAAAAAAAAAAAAAAg3SGkImRmZeYlZSUkI+Pi4qKiYiIh4aGhYSEg4KChIOD + j46OpKOjs7Kyt7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2 + t7a2t7a2t7a2t7a2trOtpY1CoIIioIIioIIioIIiqZZct7a1t7a2t7a2t7a2t6uHuZcouZgoeXEoN0gn + OV2zPGbgMWPiClXiCVXkDmzqILf3Js/7AAAAAAAAAAAAAAAAcmB2aVZtaVZtcmV0goCBg4KCg4KCg4KC + g4KChoWFmJeXrKurtrW1t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2 + t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2uKNitpUnvJkowp4pyKMqyqUsAAAAAAAAAAAAAAAAAAAAAAAA + zasvamwsOEwrN042PmfYP2rqH17oCVjmFIXuJMf6JtD7AAAAAAAAAAAAAAAAAAAAaVZtaVZtaVZtaVZt + cGJzgX+Bg4KCjIuLoJ+fs7Kyt7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2 + t7a2t7a2t7a2t7a2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA37Yv37Yv37Yv37Yv37YvAAAAAAAAAAAA + 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 + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA3rYvV54uIZQuIZQuJcPP + JtD7AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwrEv + KZUuIZQuIZUxJs70JtD7AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAA37Yve6QuIZQuIZQuIp9SJtD6AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAA17UvM5cuIZQuIZQuI6p6JtD7AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAnaovIZQuIZQuIZQuJLWgAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAV54uIZQuIZQuIZUwJcDDAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJ5UuIZQu + IZQuIZc5JcXXAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAARZ46IZQuIZQuIZQuIpxIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAM5xAIZQuIZQuIZQuJ6BTAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOaBHIZQuIZQuIpQvAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOJ9GI5UwAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAA/////////////////////////////////////////H/////////wP/// + /////4A////////+AD////////gAH///////wAAf//////8AAB//////+AAAD//////gAYAP/////4AP + AA/////8AD4AD/////AB/AAH////gAf8AAf///4AP/ggh///+AD/8CCD///AA//gYYP//wAf/+Dhg//4 + AH//wAAD/+AD//gAAAH/AA/AAAAAA/wAAAAAAAAD4AAAAAAAAAPAAAAAAA/AB8AAAAP8H4AHwAAP//g/ + AA/AP///8D4AD+A////wfAYP+B///+DwBB/8D///wOAEH/4H//+BwAQ//wP//4MAAD//gf//BgMAP//A + f/4EBwB//+A//gAOAH//8B/8AD4A///4D/gAfgD///wH8AD+Af///gPwA/4B////AeAH/AH////AwA/8 + A////+AAP/wD////8AB//Af////4Af/8B/////wD//gH/////gf/+A//////H//4D/////////gf//// + ////+B/////////4H/////////A/////////8D/////////wf/////////B/////////8H/////////g + /////////+D/////////4f/////////z//////////////////////////////////8oAAAAQAAAAIAA + AAABACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADKpUACs4ckEMObMAgAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACyqpcwjV0YpYlU + CvSLVgvSmWcTMQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAt7a2Dre2 + tli3trbBn4Ve/HpCAf96QgH/ekIB/4dSCM3FnzkGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALe2 + tgK3trYlt7a2g7e2tt63trb9squW/41hFP96QgH/ekIB/3pCAf+JVAn4yZ0oIwAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAt7a2Bre2tkW3trast7a28re2tv+3trb/trOu/6SLPf+LXgv/ekIB/3pCAf96QgH/pG8P/9+2 + L1QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAC3trYVt7a2b7e2ttW3trb+t7a2/7e2tv+3trb/t7W1/6mXXf+fgSH/jF0H/3xF + Af+FSAH/nloD/8+ZG//fti+iAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAt7a2BLe2tjm3trabt7a277e2tv+3trb/t7a2/7e2tv+3trb/t7a2/6+k + gv+ggyT/oX8e/6JsCP+oYwT/rF8C/798CP/WpSP/37Yv4N+2LwoAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC3trYSt7a2Xre2tsO3trb4t7a2/7e2tv+3trb/t7a2/7e2 + tv+3trb/t7a2/7SvoP+ukDP/w58p/86dIP/Dggn/unQG/7lzBv/Dggn/268q/9+2L/vfti88AAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAt7a2Are2tiS3traJt7a24re2tv+3trb/t7a2/7e2 + tv+3trb/t7a2/7e2tv+3trb/t7a2+Li2s7TQsEfr3bQv/9+2L//Qmhr/w4IJ/8mNEv/Egwr/xocN/960 + Lv/fti//37YvgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC3trYHt7a2T7e2trO3trb6t7a2/7e2 + tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2tv+3trbgt7a2g7m2sSLfti9937Yv/9+2L//ftS//ypAT/8eJ + Dv/csCv/yY4R/8uQE//fti//37Yv/9+2L8wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALe2tiG3trZ2t7a22be2 + tv23trb/t7a2/7e2tv+3trb/t7a2/7e2tv+3trb/t7a297e2try3trZat7a2DAAAAADfti8/37Yv9t+2 + L//fti//3rQt/8aIDv/YqCX/37Yv/9GcHP/Qmhr/37Yv/9+2L//fti/637YvHAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC3trYHt7a2Obe2 + tqS3trbtt7a2/7e2tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2tv63trbqt7a2lre2tjO3trYEAAAAAAAA + AADfti8W37Yv1d+2L//fti//37Yv/9uwKv/Qmxv/37Yv/9+2L//Zqyf/1aMh/9+2L//fti//37Yv/9+2 + L2MAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALe2 + tgy3trZlt7a2xre2tv23trb/t7a2/7e2tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2ttK3trZqt7a2FgAA + AAAAAAAAAAAAAAAAAADfti8C37Yvo9+2L//fti//37Yv/9+2L//brin23rQu/9+2L//fti//3rUu/duv + KvDfti//37Yv/9+2L//fti+p37YvAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAC3trYBt7a2L7e2to+3trbut7a2/7e2tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2tv+3trbyt7a2pre2 + tkC3trYBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA37YvYt+2L/zfti//37Yv/9+2L//fti/p37YvrN+2 + L//fti//37Yv/9+2L/Dfti+m37Yv/9+2L//fti//37Yv6d+2Lw8AAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAALe2tg63trZSt7a2u7e2tva3trb/t7a2/7e2tv+3trb/t7a2/7e2tv+3trb/t7a2/re2 + ttq3trZ8t7a2ILe2tgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA37YvKN+2L+/fti//37Yv/9+2 + L//fti/837YvYN+2L7Xfti//37Yv/9+2L//fti/G37YvV9+2L/7fti//37Yv/9+2L//fti9DAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAC3trYCt7a2H7e2tn63trbZt7a2/re2tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2 + tv+3trb3t7a2ure2tlO3trYNAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA37YvDN+2 + L8Lfti//37Yv/9+2L//fti//37Yvod+2LxHfti/b37Yv/9+2L//fti//37YvlN+2Lxbfti/037Yv/9+2 + L//fti//37YvjQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAALe2tgK3trZBt7a2pre2tvS3trb/t7a2/7e2tv+3trb/t7a2/7e2 + tv+3trb/t7a2/7e2tuy3traPt7a2Lre2tgEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAN+2L4Xfti//37Yv/9+2L//fti//37Yv3d+2LxDfti8i37Yv9d+2L//fti//37Yv/9+2 + L2QAAAAA37Yvv9+2L//fti//37Yv/9+2L9Dfti8HAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAt7a2Fre2tmy3trbUt7a2/re2tv+3trb/t7a2/7e2 + tv+3trb/t7a2/7e2tv+3trb9t7a2xbe2tmO3trYNAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAN+2L0rfti/337Yv/9+2L//fti//37Yv9N+2Lz4AAAAA37YvS961 + L//asi7/1q8t/9KsLv3Esndyt7a2X7+oXrqwkyf/YWcl/zpSI/91dyf4sp0sKwAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALe2tgS3trY0t7a2lre2tum3trb/t7a2/7e2 + tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2tu23traht7a2O7e2tgcAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAt7a2CMasViTXry3f1a4t/9CqLP/KpSr/xKAq/8Ot + aNu3traft7a2tq+dZN+niCP/pYYj/6OFI/+jiC//tbGp/7e2tv+XmJf/LEAj/wYsHv8GLB7/By0e/yhO + On8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAt7a2Dbe2tli3tra/t7a2+Le2 + tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2tv+3trb8t7a22re2tnW3trYeAAAAAAAAAAAAAAAAAAAAALe2 + tgK3trYFt7a2Cbe2tg23trYbt7a2Nbe2tky3trZlt7a2fLe2tpS3tratt7a2xLe1s9Spk0vspIUj/6CC + Iv+ggiL/oIIi/6aQSv+2tLD/t7a2/7e2tv+ok1P/oIIi/6CCIv+ggiL/pIw+/7W0tf+AjLD/MlCk/wow + M/8GLB7/Biwe/wYsHv8eRz+4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC3trYjt7a2hLe2 + tuG3trb/t7a2/7e2tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2tvu3trawt7a2Ure2tiC3trYvt7a2Rbe2 + tlq3trZ0t7a2i7e2tqK3tra6t7a2zbe2tuO3trb2t7a2/7e2tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2 + tv+uoXr/oIIi/6CCIv+ggiL/oIIi/6KHL/+0r6L/t7a2/7e2tv+3tbT/o4k2/6CCIv+ggiL/oIIi/56O + Wv9dcaz/LUyn/x5Gpv8GNGD/Biwe/wYsHv8GLSD/G0xIcgAAAAAAAAAAAAAAAAAAAAAAAAAAt7a2Cbe2 + tkm3travt7a297e2tv+0s7P/srGx/66trf+rqqr/p6am/6Khof+cm5v/np2d/6uqqv60s7Pht7a25Le2 + tvW3trb4t7a2+7e2tv23trb/t7a2/7e2tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2 + tv+3trb/t7a2/7e2tv+yrJj/oYUr/6CCIv+ggiL/oIIi/6CCI/+vpYX/t7a2/7e2tv+3trb/tLCk/6GF + Kv+ggiL/oIIi/4JvIf88Tm7/LUyn/y5Oq/8PQ6z/BkGr/wc7eP8IQGD/F4Sd4ym42xYAAAAAAAAAAAAA + AACTg5Ygg3SGgJCJkdOZl5j7lZSU/5CPj/+Lior/iYiI/4eGhv+FhIT/g4KC/4SDg/+Pjo7/pKOj/7Oy + sv+3trb/t7a2/7e2tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2 + tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2tv+2s63/pY1C/6CCIv+ggiL/oIIi/6CCIv+pllz6t7a18be2 + tui3trbgt7a2zberh825lyj/uZgo/3lxKP83SCf/OV2z/zxm4P8xY+L/ClXi/wlV5P8ObOr/ILf3/ybP + +5EAAAAAAAAAAAAAAACLeo4acmB23mlWbf9pVm3/cmV0/4KAgf+DgoL/g4KC/4OCgv+DgoL/hoWF/5iX + l/+sq6v/trW1/7e2tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2 + tv+3trb/t7a2/7e2tv+3trb/t7a2/re2tvy3trb6t7a2+Le2tuy3trbWuKNi9baVJ/+8mSj/wp4p/8ij + Kv/KpSzUtaqGN7e2tiO3trYTt7a2BAAAAADfti90zasv/2psLP84TCv/N042/z5n2P8/aur/H17o/wlY + 5v8Uhe7/JMf6/ybQ+/wm0PsvAAAAAAAAAAAAAAAAjHqPcWlWbf9pVm3/aVZt/2lWbf9wYnP/gX+B/4OC + gv+Mi4v/oJ+f/7Oysv+3trb/t7a2/7e2tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2 + tv+3trb9t7a27Le2tta3trbDt7a2rre2tpa3trZ+t7a2Zre2tk+3trY4t7a2I7e2tg4AAAAA37YvTt+2 + L/zfti//37Yv/9+2L//fti/y37YvNwAAAAAAAAAAAAAAAAAAAAAAAAAAlIczp0pYK/83TCv/N0wr/zhR + Tv8/aeT/O2nq/w9g6P8cpPT/Js/7/ybQ+/8m0Pu9JtD7BAAAAAAAAAAAAAAAAI59kVVrWG/+aVZt/2lW + bf9pVm3/a1hv/31sgP+moqX/tbS0/7e2tv+3trb/t7a2/7e2tve3trbot7a227e2ts63tra3t7a2obe2 + toe3trZvt7a2Wbe2tkG3trYmt7a2Ebe2tgy3trYHt7a2AwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAA37YvH9+2L+Pfti//37Yv/9+2L//fti/+37YvcN+2LwEAAAAAAAAAAAAAAAA/auoMP2rqgzlR + SPs3TCv/N0wr/zdMK/86V27/P2rq/y567PchvPj/JtD7/ybQ+/8m0Pv+JtD7WQAAAAAAAAAAAAAAAAAA + AACjk6UJfmqBoHFddvx0X3n/hm+M/5p/of+mia3/s5q5/ruyvZu3trZht7a2Sre2tjG3trYit7a2F7e2 + tg23trYDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAA37YvCN+2L7Lfti//37Yv/9+2L//fti//37Yvst+2LwcAAAAAAAAAAAAA + AAA/auonP2rqvD9p4f43TC7/N0wr/zdMK/83TCv/O1yQ+D5v65YnyfrTJtD7/ybQ+/8m0Pv/JtD76CbQ + +wsAAAAAAAAAAAAAAAAAAAAAAAAAAM2q1gPCocuAyafS/c2q1v/Nqtb/zarW/82q1v/NqtbXzarWKwAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAN+2L3Lfti/937Yv/9+2L//fti//37Yv5t+2 + LxwAAAAAAAAAAD9q6gE/aupSP2rq5z9q6v89ZMH/N0wr/zdMK/83TCv/RFQr/2Nxbmonzfs8JtD7+ybQ + +/8m0Pv/JtD7/ybQ+40AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM2q1m7Nqtb4zarW/82q + 1v/Nqtb/zarW/82q1ubNqtY2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAN+2Lznfti/z37Yv/9+2 + L//fti//37Yv+N+2L00AAAAAAAAAAD9q6g4/auqSP2rq/z9q6v8/aur/PF2Z/zdMK/83TCv/ZGgs/8uq + L/7fti8SJtD7oCbQ+/8m0Pv/JtD7/ybQ+/Qm0PstAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AADNqtYCzarWXc2q1vTNqtb/zarW/82q1v/Nqtb/zarW682q1kMAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAN+2 + Lw3fti/W37Yv/9+2L//fti//37Yv/9+2L43fti8CAAAAAD9q6i8/aurKP2rq/j9q6v8/aur/P2rq/zpY + d/89UCv/jIIt/9myL//fti/cQMzeHibQ++sm0Pv/JtD7/ybQ+/8m0Pu6JtD7AQAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAM2q1gHNqtZNzarW8s2q1v/Nqtb/zarW/82q1v/NqtbwzarWUM2q + 1gEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAN+2LwTfti+Y37Yv/9+2L//fti//37Yv/9+2L8nfti8PP2rqBD9q6mI/aursP2rq/z9q + 6v8/aur/P2rq/z9p5v9NX1b/spou/961L//fti//37YvrifQ+m8m0Pv/JtD7/ybQ+/8m0Pv+JtD7VgAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM2q1kDNqtbtzarW/82q + 1v/Nqtb/zarW/82q1vfNqtZeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADfti9V37Yv+9+2L//fti//37Yv/9+2L/Lfti8sP2rqET9q + 6p4/aur+P2rq/z9q6v8/aur/P2rq/z9q6v9Lb9m+zaw299+2L//fti//37Yv/9y2MoEm0PvUJtD7/ybQ + +/8m0Pv/JtD73ybQ+xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAzarWNM2q1t7Nqtb/zarW/82q1v/Nqtb/zarW/M2q1nHNqtYCAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADfti8m37Yv6N+2L//fti//37Yv/9+2 + L/3fti9oQGrpOj9q6tU/aur/P2rq/z9q6v8/aur/P2rq/z9q6vI/aupv0rA+Mt+2L//fti//37Yv/9+2 + L/6LwouOJtD7/SbQ+/8m0Pv/JtD7/ybQ+4UAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADNqtYtzarW1c2q1v/Nqtb/zarW/82q1v/Nqtb6zarWg82q + 1gUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADfti8J37Yvvd+2 + L//fti//37Yv/9+2L//ZszawRGzkdD9q6vM/aur/P2rq/z9q6v8/aur/P2rq/z9q6tQ/auo5P2rqAd+2 + L2Lfti//37Yv/9+2L//etjD3RsvWxCbQ+/8m0Pv/JtD7/ybQ+/km0PsjAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM2q1iDNqtbQzarW/82q + 1v/Nqtb/zarW/82q1vzNqtaRzarWBgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAA37Yve9+2L/3fti//37Yv/9+2L//NrT/wVHTRxT9q6v4/aur/P2rq/z9q6v8/aur/P2rq/D9q + 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 + Lv8hlC7/JcPP/ybQ++wm0PsgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AADfti94wrEv/ymVLv8hlC7/IZUx/ybO9P8m0PukJtD7AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAA37Yvp3ukLv8hlC7/IZQu/yKfUv8m0Pr+JtD7PwAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA37YvCNe1L9Ezly7/IZQu/yGULv8jqnr/JtD7zibQ + +wkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAN+2Lxmdqi/0IZQu/yGU + Lv8hlC7/JLWg/ybQ+20AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AADUtC87V54u/yGULv8hlC7/IZUw/yXAw/Qm0PsRAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAk6kvbyeVLv8hlC7/IZQu/yGXOf8lxdegAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEWeOqshlC7/IZQu/yGULv8inEj6Js/4PQAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAznEDQIZQu/yGU + Lv8hlC7/J6BT0CbQ+wQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAOaBHnCGULv8hlC7/IpQv/zmhTHEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAFKrYQ44n0aRI5UwtT2hS35suH0JAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/////////////////////////////8f/// + //////g/////////wB////////4AH///////+AAf///////gAB///////wAAD//////8AAAP/////+AA + AA//////gAAAD/////4AAgAH////8AAMAAf////AAHgAA////gAB+AAD///4AAfwAAP//8AAP+AAA/// + AAD/4ACB//wAB//AQAH/4AAf/wAAAf+AAPAAAAAB/gAAAAAAAAHwAAAAAAAAAcAAAAAAAAADgAAAAAAA + gAOAAAAACA+AA4AAAA/wDgAHgAB//+AcAAfAH///4DAAD/AP///AYAAP8Af//4BAAA/4Af//AAAAH/4B + //8AAAAf/wB//gAAAD//gD/8AAAAP//AH/wABgA//+AP+AAOAH//8AfwADwAf//4A+AAfAD///wB4AD8 + AP///gDAA/wB////AAAH/AH///+AAA/4Af///+AAP/gD////8AB/+AP////4AP/4A/////wD//AH//// + /gf/8Af/////H//wD/////////AP////////8A/////////wH////////+Af////////4D/////////g + P////////+B/////////4H/////////gf////////+D/////////4P////////////////////////// + ////////iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAAgAElEQVR42u29eZRk133f97n3 + bbV19To9K2YADDAABrsAgYsIQhRNygSphZJNndhiZFG2adOREx2dJMfSsSImIn0sKUpkRfQRHZ1IpkxZ + NBcllEGJMSlCYkhQIIhlsGOAGcz0LD29d+1vub/88bp7umd6qa6uXqrqfs9pDKan3qt6r97ve7/f3/3d + 3wULCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLC4uOgLK3wKLT + 8S8e+6hcKF9iqjbD+dIYAFppsm6WG4qHGc4M8W8/+Mf2WV8F2t4Ci07H67NnmaxNU4kqS78TEWITM1uf + 41LlMh/8o3eLvVNWAVh0ET702R+WWlznzflz649yC2rgrpGT/Luf/Jx95i0BWHQ6fuqz75Op+hRTtWkS + SZp40BU5L0efX+CrP/uEfe6tBbDoZEzXp6lE1aaCH0AQwiSkkTT4+f/7p60dsARg0an4n7/638lUfWqF + 528GkYmITcx8WLI30RKARadirHyxZfdaCss8M37K3kRLABadilpcR6R1FS9YB2AJwKJj0YjDLZ/jN/7s + EcsClgAsehVxPMW/e+wesQRgYdFhyHqZLZ9jLkxnBCwBWFh0GHw3j1Jbm8qfjxLiJLYEYB8ni05D5J1E + tljDdr4u1AxEz5wQSwAWFh2EP/jALyuji6BatwIToVBN7L107S2w6ESIsw9QENdbOr6aQN3Q8yRgFYBF + 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 + GBgsBv8PNAlKsBafvBgAAAAASUVORK5CYII= + + + \ No newline at end of file diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs b/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs new file mode 100644 index 00000000..2503a1a7 --- /dev/null +++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs @@ -0,0 +1,54 @@ +using System; +using System.Linq; +using System.Drawing.Design; +using System.ComponentModel; +using System.Windows.Forms.Design; +using System.Windows.Forms; +using System.Reactive.Linq; +using System.Numerics; +using Bonsai.Design; + + +namespace OpenEphys.Onix1.Design +{ + /// + /// Provides a user interface editor that displays a dialog for selecting + /// members of a workflow expression type. + /// + public class SpatialTransformMatrixEditor : DataSourceTypeEditor + { + public SpatialTransformMatrixEditor() + : base(DataSource.Input, typeof(void)) + { + } + + /// + public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context) + { + return UITypeEditorEditStyle.Modal; + } + + protected virtual IObservable> GetData(IObservable> source) + { + return source.Merge().Select(coordinate => (Tuple)coordinate); + } + + public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value) + { + var editorService = (IWindowsFormsEditorService)provider.GetService(typeof(IWindowsFormsEditorService)); + if (context != null && editorService != null) + { + var source = GetDataSource(context, provider); + var dataFrames = GetData(source.Output); + using (var visualizerDialog = new SpatialTransformMatrixDialog(dataFrames)) + { + if (editorService.ShowDialog(visualizerDialog) == DialogResult.OK && visualizerDialog.ApplySpatialTransform) + { + return visualizerDialog.SpatialTransform; + } + } + } + return base.EditValue(context, provider, value); + } + } +} diff --git a/OpenEphys.Onix1/SpatialTransform.cs b/OpenEphys.Onix1/SpatialTransform.cs new file mode 100644 index 00000000..a74dbb22 --- /dev/null +++ b/OpenEphys.Onix1/SpatialTransform.cs @@ -0,0 +1,32 @@ +using Bonsai; +using System; +using System.ComponentModel; +using System.Linq; +using System.Reactive.Linq; +using System.Numerics; + +namespace OpenEphys.Onix1 +{ + /// + /// Represents an operator that groups the elements of an observable + /// sequence according to the specified key. + /// + [DefaultProperty(nameof(SpatialTransformMatrix))] + [Description("Groups the elements of an observable sequence according to the specified key.")] + public class SpatialTransform : Transform, Vector3> + { + /// + /// Gets or sets a value specifying the inner properties used as key for + /// each element in the sequence. + /// + [Description("Specifies the inner properties used as key for each element of the sequence.")] + [Editor("OpenEphys.Onix1.Design.SpatialTransformMatrixEditor, OpenEphys.Onix1.Design", DesignTypes.UITypeEditor)] + [TypeConverter(typeof(NumericRecordConverter))] + public Matrix4x4 SpatialTransformMatrix { get; set; } + + public override IObservable Process(IObservable> source) + { + return source.Select(input => Vector3.Transform(input.Item2, this.SpatialTransformMatrix)); + } + } +} From 2162dc1335a209946032653d90e456a069565630 Mon Sep 17 00:00:00 2001 From: cjsha Date: Wed, 16 Apr 2025 16:07:07 -0400 Subject: [PATCH 02/17] Address (some of) bparks feedback - Consolidate win form event handlers - Initialize SpatialTransformMatrix property to indentity matrix - Refactor some functions here and there --- .../SpatialTransformMatrixDialog.Designer.cs | 37 ++++-- .../SpatialTransformMatrixDialog.cs | 109 ++++-------------- OpenEphys.Onix1/SpatialTransform.cs | 10 +- 3 files changed, 57 insertions(+), 99 deletions(-) diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.Designer.cs b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.Designer.cs index 2eafd34d..eea32388 100644 --- a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.Designer.cs +++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.Designer.cs @@ -157,6 +157,7 @@ private void InitializeComponent() this.tableLayoutPanelCoordinates.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 30F)); this.tableLayoutPanelCoordinates.Size = new System.Drawing.Size(618, 150); this.tableLayoutPanelCoordinates.TabIndex = 5; + this.tableLayoutPanelCoordinates.Tag = "6"; // // textBoxUserCoordinate3 // @@ -166,8 +167,9 @@ private void InitializeComponent() this.textBoxUserCoordinate3.Name = "textBoxUserCoordinate3"; this.textBoxUserCoordinate3.Size = new System.Drawing.Size(220, 20); this.textBoxUserCoordinate3.TabIndex = 39; + this.textBoxUserCoordinate3.Tag = "7"; this.textBoxUserCoordinate3.Text = "x\'3, y\'3, z\'3"; - this.textBoxUserCoordinate3.TextChanged += new System.EventHandler(this.textBoxUserCoordinate3_TextChanged); + this.textBoxUserCoordinate3.TextChanged += new System.EventHandler(this.textBoxUserCoordinate_TextChanged); // // textBoxUserCoordinate2 // @@ -177,8 +179,9 @@ private void InitializeComponent() this.textBoxUserCoordinate2.Name = "textBoxUserCoordinate2"; this.textBoxUserCoordinate2.Size = new System.Drawing.Size(220, 20); this.textBoxUserCoordinate2.TabIndex = 38; + this.textBoxUserCoordinate2.Tag = "6"; this.textBoxUserCoordinate2.Text = "x\'2, y\'2, z\'2"; - this.textBoxUserCoordinate2.TextChanged += new System.EventHandler(this.textBoxUserCoordinate2_TextChanged); + this.textBoxUserCoordinate2.TextChanged += new System.EventHandler(this.textBoxUserCoordinate_TextChanged); // // textBoxUserCoordinate1 // @@ -188,8 +191,9 @@ private void InitializeComponent() this.textBoxUserCoordinate1.Name = "textBoxUserCoordinate1"; this.textBoxUserCoordinate1.Size = new System.Drawing.Size(220, 20); this.textBoxUserCoordinate1.TabIndex = 37; + this.textBoxUserCoordinate1.Tag = "5"; this.textBoxUserCoordinate1.Text = "x\'1, y\'1, z\'1"; - this.textBoxUserCoordinate1.TextChanged += new System.EventHandler(this.textBoxUserCoordinate1_TextChanged); + this.textBoxUserCoordinate1.TextChanged += new System.EventHandler(this.textBoxUserCoordinate_TextChanged); // // textBoxUserCoordinate0 // @@ -199,8 +203,9 @@ private void InitializeComponent() this.textBoxUserCoordinate0.Name = "textBoxUserCoordinate0"; this.textBoxUserCoordinate0.Size = new System.Drawing.Size(220, 20); this.textBoxUserCoordinate0.TabIndex = 36; + this.textBoxUserCoordinate0.Tag = "4"; this.textBoxUserCoordinate0.Text = "x\'0, y\'0, z\'0"; - this.textBoxUserCoordinate0.TextChanged += new System.EventHandler(this.textBoxUserCoordinate0_TextChanged); + this.textBoxUserCoordinate0.TextChanged += new System.EventHandler(this.textBoxUserCoordinate_TextChanged); // // textBoxTS4231Coordinate3 // @@ -212,8 +217,9 @@ private void InitializeComponent() this.textBoxTS4231Coordinate3.Size = new System.Drawing.Size(220, 20); this.textBoxTS4231Coordinate3.TabIndex = 33; this.textBoxTS4231Coordinate3.TabStop = false; + this.textBoxTS4231Coordinate3.Tag = "3"; this.textBoxTS4231Coordinate3.Text = "x3, y3, z3"; - this.textBoxTS4231Coordinate3.TextChanged += new System.EventHandler(this.textBoxTS4231Coordinate3_TextChanged); + this.textBoxTS4231Coordinate3.TextChanged += new System.EventHandler(this.textBoxTS4231Coordinate_TextChanged); // // textBoxTS4231Coordinate2 // @@ -225,8 +231,9 @@ private void InitializeComponent() this.textBoxTS4231Coordinate2.Size = new System.Drawing.Size(220, 20); this.textBoxTS4231Coordinate2.TabIndex = 32; this.textBoxTS4231Coordinate2.TabStop = false; + this.textBoxTS4231Coordinate2.Tag = "2"; this.textBoxTS4231Coordinate2.Text = "x2, y2, z2"; - this.textBoxTS4231Coordinate2.TextChanged += new System.EventHandler(this.textBoxTS4231Coordinate2_TextChanged); + this.textBoxTS4231Coordinate2.TextChanged += new System.EventHandler(this.textBoxTS4231Coordinate_TextChanged); // // textBoxTS4231Coordinate1 // @@ -238,8 +245,9 @@ private void InitializeComponent() this.textBoxTS4231Coordinate1.Size = new System.Drawing.Size(220, 20); this.textBoxTS4231Coordinate1.TabIndex = 31; this.textBoxTS4231Coordinate1.TabStop = false; + this.textBoxTS4231Coordinate1.Tag = "1"; this.textBoxTS4231Coordinate1.Text = "x1, y1, z1"; - this.textBoxTS4231Coordinate1.TextChanged += new System.EventHandler(this.textBoxTS4231Coordinate1_TextChanged); + this.textBoxTS4231Coordinate1.TextChanged += new System.EventHandler(this.textBoxTS4231Coordinate_TextChanged); // // buttonMeasure3 // @@ -247,9 +255,10 @@ private void InitializeComponent() this.buttonMeasure3.Name = "buttonMeasure3"; this.buttonMeasure3.Size = new System.Drawing.Size(80, 24); this.buttonMeasure3.TabIndex = 29; + this.buttonMeasure3.Tag = "3"; this.buttonMeasure3.Text = "Measure"; this.buttonMeasure3.UseVisualStyleBackColor = true; - this.buttonMeasure3.Click += new System.EventHandler(this.buttonMeasure3_Click); + this.buttonMeasure3.Click += new System.EventHandler(this.buttonMeasure_Click); // // buttonMeasure2 // @@ -257,9 +266,10 @@ private void InitializeComponent() this.buttonMeasure2.Name = "buttonMeasure2"; this.buttonMeasure2.Size = new System.Drawing.Size(80, 24); this.buttonMeasure2.TabIndex = 26; + this.buttonMeasure2.Tag = "2"; this.buttonMeasure2.Text = "Measure"; this.buttonMeasure2.UseVisualStyleBackColor = true; - this.buttonMeasure2.Click += new System.EventHandler(this.buttonMeasure2_Click); + this.buttonMeasure2.Click += new System.EventHandler(this.buttonMeasure_Click); // // buttonMeasure1 // @@ -268,9 +278,10 @@ private void InitializeComponent() this.buttonMeasure1.Name = "buttonMeasure1"; this.buttonMeasure1.Size = new System.Drawing.Size(80, 24); this.buttonMeasure1.TabIndex = 23; + this.buttonMeasure1.Tag = "1"; this.buttonMeasure1.Text = "Measure"; this.buttonMeasure1.UseVisualStyleBackColor = true; - this.buttonMeasure1.Click += new System.EventHandler(this.buttonMeasure1_Click); + this.buttonMeasure1.Click += new System.EventHandler(this.buttonMeasure_Click); // // labelCoordinate3 // @@ -307,9 +318,10 @@ private void InitializeComponent() this.buttonMeasure0.Name = "buttonMeasure0"; this.buttonMeasure0.Size = new System.Drawing.Size(80, 24); this.buttonMeasure0.TabIndex = 1; + this.buttonMeasure0.Tag = "0"; this.buttonMeasure0.Text = "Measure"; this.buttonMeasure0.UseVisualStyleBackColor = true; - this.buttonMeasure0.Click += new System.EventHandler(this.buttonMeasure0_Click); + this.buttonMeasure0.Click += new System.EventHandler(this.buttonMeasure_Click); // // labelHeaderTS4231 // @@ -342,8 +354,9 @@ private void InitializeComponent() this.textBoxTS4231Coordinate0.Size = new System.Drawing.Size(220, 20); this.textBoxTS4231Coordinate0.TabIndex = 30; this.textBoxTS4231Coordinate0.TabStop = false; + this.textBoxTS4231Coordinate0.Tag = "0"; this.textBoxTS4231Coordinate0.Text = "x0, y0, z0"; - this.textBoxTS4231Coordinate0.TextChanged += new System.EventHandler(this.textBoxTS4231Coordinate0_TextChanged); + this.textBoxTS4231Coordinate0.TextChanged += new System.EventHandler(this.textBoxTS4231Coordinate_TextChanged); // // labelHeaderUser // diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs index 1098ac5d..a07d07a4 100644 --- a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs +++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs @@ -3,6 +3,9 @@ using System.Numerics; using System.Windows.Forms; using System.Reactive.Linq; +using System.Collections.Generic; +using Bonsai.Reactive; +using System.Reflection; namespace OpenEphys.Onix1.Design { @@ -11,7 +14,7 @@ public partial class SpatialTransformMatrixDialog : Form private const byte NumMeasurements = 100; private bool[] InputsValid = { false, false, false, false, false, false, false, false }; - + private IObservable> PositionDataSource; private Vector3[] TS4231Coordinates = { Vector3.Zero, Vector3.Zero, Vector3.Zero, Vector3.Zero }; @@ -26,17 +29,7 @@ internal SpatialTransformMatrixDialog(IObservable> positionD PositionDataSource = positionDataSource; } - private bool CheckInputValidity(string userInput) - { - string[] serInputSplit = userInput.Split(','); - if (serInputSplit.Length != 3) - { - return false; - } - return serInputSplit.All(floatCandidate => float.TryParse(floatCandidate, out _)); - } - - private void DisableButtons() + private void DisableButtons() { buttonMeasure0.Enabled = false; buttonMeasure1.Enabled = false; @@ -45,7 +38,7 @@ private void DisableButtons() buttonCalculate.Enabled = false; } - private void EnableButtons() + private void EnableButtons() { buttonMeasure0.Invoke((Action)delegate { @@ -69,11 +62,12 @@ private void EnableButtons() }); } - private void buttonMeasure_Click(byte coordinate) + private void buttonMeasure_Click(object sender, EventArgs e) { TextBox[] ts4231TextBoxes = { textBoxTS4231Coordinate0, textBoxTS4231Coordinate1, textBoxTS4231Coordinate2, textBoxTS4231Coordinate3 }; + var index = int.Parse((string)((Button)sender).Tag); - textBoxStatus.AppendText(string.Format("Measuring coordinate {0}...", coordinate) + Environment.NewLine); + textBoxStatus.AppendText(string.Format("Measuring coordinate {0}...", index) + Environment.NewLine); DisableButtons(); @@ -87,7 +81,7 @@ private void buttonMeasure_Click(byte coordinate) { textBoxStatus.Invoke((Action)delegate { - textBoxStatus.AppendText(string.Format("Measurements at coordinate {0} complete.", coordinate) + textBoxStatus.AppendText(string.Format("Measurements at coordinate {0} complete.", index) + Environment.NewLine + Environment.NewLine + "Awaiting user input..." + Environment.NewLine); }); EnableButtons(); @@ -107,86 +101,33 @@ private void buttonMeasure_Click(byte coordinate) (acc, current) => acc + current, acc => { - TS4231Coordinates[coordinate] = acc / NumMeasurements; - ts4231TextBoxes[coordinate].Invoke((Action)delegate + TS4231Coordinates[index] = acc / NumMeasurements; + ts4231TextBoxes[index].Invoke((Action)delegate { - ts4231TextBoxes[coordinate].Text = string.Format("{0}, {1}, {2}", - TS4231Coordinates[coordinate].X, - TS4231Coordinates[coordinate].Y, - TS4231Coordinates[coordinate].Z); + ts4231TextBoxes[index].Text = string.Format("{0}, {1}, {2}", + TS4231Coordinates[index].X, + TS4231Coordinates[index].Y, + TS4231Coordinates[index].Z); }); - return TS4231Coordinates[coordinate]; + return TS4231Coordinates[index]; }) .Subscribe(); sharedPositionDataGroups.Connect(); - } - - private void textBoxTS4231Coordinate0_TextChanged(object sender, EventArgs e) - { - - buttonCalculate.Enabled = InputsValid.All(inputValid => inputValid); - } - - private void textBoxTS4231Coordinate1_TextChanged(object sender, EventArgs e) - { - buttonCalculate.Enabled = InputsValid.All(inputValid => inputValid); - } - - private void textBoxTS4231Coordinate2_TextChanged(object sender, EventArgs e) - { - buttonCalculate.Enabled = InputsValid.All(inputValid => inputValid); - } - - private void textBoxTS4231Coordinate3_TextChanged(object sender, EventArgs e) - { - buttonCalculate.Enabled = InputsValid.All(inputValid => inputValid); - } - - private void buttonMeasure0_Click(object sender, EventArgs e) - { - buttonMeasure_Click(0); - InputsValid[0] = true; - } - - private void buttonMeasure1_Click(object sender, EventArgs e) - { - buttonMeasure_Click(1); - InputsValid[1] = true; - } - - private void buttonMeasure2_Click(object sender, EventArgs e) - { - buttonMeasure_Click(2); - InputsValid[2] = true; - } - - private void buttonMeasure3_Click(object sender, EventArgs e) - { - buttonMeasure_Click(3); - InputsValid[3] = true; - } - private void textBoxUserCoordinate0_TextChanged(object sender, EventArgs e) - { - InputsValid[4] = CheckInputValidity(textBoxUserCoordinate0.Text); - buttonCalculate.Enabled = InputsValid.All(inputValid => inputValid); - } - - private void textBoxUserCoordinate1_TextChanged(object sender, EventArgs e) - { - InputsValid[5] = CheckInputValidity(textBoxUserCoordinate1.Text); - buttonCalculate.Enabled = InputsValid.All(inputValid => inputValid); - } + } - private void textBoxUserCoordinate2_TextChanged(object sender, EventArgs e) + private void textBoxTS4231Coordinate_TextChanged(object sender, EventArgs e) { - InputsValid[6] = CheckInputValidity(textBoxUserCoordinate2.Text); + var index = int.Parse((string)((TextBox)sender).Tag); + InputsValid[index] = true; buttonCalculate.Enabled = InputsValid.All(inputValid => inputValid); } - private void textBoxUserCoordinate3_TextChanged(object sender, EventArgs e) + private void textBoxUserCoordinate_TextChanged(object sender, EventArgs e) { - InputsValid[7] = CheckInputValidity(textBoxUserCoordinate3.Text); + var index = int.Parse((string)((TextBox)sender).Tag); + string[] serInputSplit = ((TextBox)sender).Text.Split(','); + InputsValid[index] = serInputSplit.Length == 3 ? serInputSplit.All(floatCandidate => float.TryParse(floatCandidate, out _)) : false; buttonCalculate.Enabled = InputsValid.All(inputValid => inputValid); } diff --git a/OpenEphys.Onix1/SpatialTransform.cs b/OpenEphys.Onix1/SpatialTransform.cs index a74dbb22..49dab035 100644 --- a/OpenEphys.Onix1/SpatialTransform.cs +++ b/OpenEphys.Onix1/SpatialTransform.cs @@ -12,17 +12,21 @@ namespace OpenEphys.Onix1 /// sequence according to the specified key. /// [DefaultProperty(nameof(SpatialTransformMatrix))] - [Description("Groups the elements of an observable sequence according to the specified key.")] + [Description("Transforms 3D coordinates from one reference frame to another.")] public class SpatialTransform : Transform, Vector3> { /// /// Gets or sets a value specifying the inner properties used as key for /// each element in the sequence. /// - [Description("Specifies the inner properties used as key for each element of the sequence.")] + [Description("Spatial Transform Matrix")] [Editor("OpenEphys.Onix1.Design.SpatialTransformMatrixEditor, OpenEphys.Onix1.Design", DesignTypes.UITypeEditor)] [TypeConverter(typeof(NumericRecordConverter))] - public Matrix4x4 SpatialTransformMatrix { get; set; } + public Matrix4x4 SpatialTransformMatrix { get; set; } = new Matrix4x4( + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1); public override IObservable Process(IObservable> source) { From cd82df1d273371a39de2e75807fb7e52e5280df0 Mon Sep 17 00:00:00 2001 From: cjsha Date: Fri, 18 Apr 2025 12:00:54 -0400 Subject: [PATCH 03/17] Use ControlScheduler - This is a more elegant than using the `WinForm.Control.Invoke` pattern to avoid cross-threading errors --- .../SpatialTransformMatrixDialog.cs | 74 +++++-------------- 1 file changed, 20 insertions(+), 54 deletions(-) diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs index a07d07a4..154c64c8 100644 --- a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs +++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs @@ -3,9 +3,7 @@ using System.Numerics; using System.Windows.Forms; using System.Reactive.Linq; -using System.Collections.Generic; -using Bonsai.Reactive; -using System.Reflection; +using Bonsai.Design; namespace OpenEphys.Onix1.Design { @@ -29,39 +27,6 @@ internal SpatialTransformMatrixDialog(IObservable> positionD PositionDataSource = positionDataSource; } - private void DisableButtons() - { - buttonMeasure0.Enabled = false; - buttonMeasure1.Enabled = false; - buttonMeasure2.Enabled = false; - buttonMeasure3.Enabled = false; - buttonCalculate.Enabled = false; - } - - private void EnableButtons() - { - buttonMeasure0.Invoke((Action)delegate - { - buttonMeasure0.Enabled = true; - }); - buttonMeasure1.Invoke((Action)delegate - { - buttonMeasure1.Enabled = true; - }); - buttonMeasure2.Invoke((Action)delegate - { - buttonMeasure2.Enabled = true; - }); - buttonMeasure3.Invoke((Action)delegate - { - buttonMeasure3.Enabled = true; - }); - buttonCalculate.Invoke((Action)delegate - { - buttonCalculate.Enabled = InputsValid.All(inputValid => inputValid); - }); - } - private void buttonMeasure_Click(object sender, EventArgs e) { TextBox[] ts4231TextBoxes = { textBoxTS4231Coordinate0, textBoxTS4231Coordinate1, textBoxTS4231Coordinate2, textBoxTS4231Coordinate3 }; @@ -69,7 +34,11 @@ private void buttonMeasure_Click(object sender, EventArgs e) textBoxStatus.AppendText(string.Format("Measuring coordinate {0}...", index) + Environment.NewLine); - DisableButtons(); + buttonMeasure0.Enabled = false; + buttonMeasure1.Enabled = false; + buttonMeasure2.Enabled = false; + buttonMeasure3.Enabled = false; + buttonCalculate.Enabled = false; var sharedPositionDataGroups = PositionDataSource.Take(NumMeasurements) .GroupBy(dataFrame => dataFrame.Item1, dataFrame => dataFrame.Item2) @@ -77,38 +46,35 @@ private void buttonMeasure_Click(object sender, EventArgs e) sharedPositionDataGroups .SelectMany(group => group.Count().Select(count => new { index = group.Key, measurementCount = count })) + .ObserveOn(new ControlScheduler(this)) .Finally(() => { - textBoxStatus.Invoke((Action)delegate - { - textBoxStatus.AppendText(string.Format("Measurements at coordinate {0} complete.", index) - + Environment.NewLine + Environment.NewLine + "Awaiting user input..." + Environment.NewLine); - }); - EnableButtons(); + textBoxStatus.AppendText(string.Format("Measurements at coordinate {0} complete.", index) + + Environment.NewLine + Environment.NewLine + "Awaiting user input..." + Environment.NewLine); + buttonMeasure0.Enabled = true; + buttonMeasure1.Enabled = true; + buttonMeasure2.Enabled = true; + buttonMeasure3.Enabled = true; + buttonCalculate.Enabled = InputsValid.All(inputValid => inputValid); }) .Subscribe(sensor => { - textBoxStatus.Invoke((Action)delegate - { - textBoxStatus.AppendText(string.Format("{1} measurements from sensor {0}.", sensor.index, sensor.measurementCount) + Environment.NewLine); - }); + textBoxStatus.AppendText(string.Format("{1} measurements from sensor {0}.", sensor.index, sensor.measurementCount) + Environment.NewLine); }); sharedPositionDataGroups .Merge() + .ObserveOn(new ControlScheduler(this)) .Aggregate( new Vector3(0, 0, 0), (acc, current) => acc + current, acc => { TS4231Coordinates[index] = acc / NumMeasurements; - ts4231TextBoxes[index].Invoke((Action)delegate - { - ts4231TextBoxes[index].Text = string.Format("{0}, {1}, {2}", - TS4231Coordinates[index].X, - TS4231Coordinates[index].Y, - TS4231Coordinates[index].Z); - }); + ts4231TextBoxes[index].Text = string.Format("{0}, {1}, {2}", + TS4231Coordinates[index].X, + TS4231Coordinates[index].Y, + TS4231Coordinates[index].Z); return TS4231Coordinates[index]; }) .Subscribe(); From f6fc49fee0bb34fbb4076659bb98d60cb28a77b3 Mon Sep 17 00:00:00 2001 From: jonnew Date: Mon, 21 Apr 2025 17:15:03 -0400 Subject: [PATCH 04/17] Move SpatialTransform into TS3241PositionData - I think the the sptial transform should be another property within TS4231PositionData that allows the user to map the base-station-based coordinate system into an external coordinate system - Defaulting to identity matrix means that this does nothing by default. --- .../SpatialTransformMatrixDialog.cs | 7 ++-- .../SpatialTransformMatrixDialog.resx | 2 +- .../SpatialTransformMatrixEditor.cs | 6 ++-- OpenEphys.Onix1/SpatialTransform.cs | 36 ------------------- OpenEphys.Onix1/TS4231V1PositionData.cs | 20 ++++++++++- 5 files changed, 27 insertions(+), 44 deletions(-) delete mode 100644 OpenEphys.Onix1/SpatialTransform.cs diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs index 154c64c8..d77e554b 100644 --- a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs +++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs @@ -13,7 +13,7 @@ public partial class SpatialTransformMatrixDialog : Form private bool[] InputsValid = { false, false, false, false, false, false, false, false }; - private IObservable> PositionDataSource; + private IObservable PositionDataSource; private Vector3[] TS4231Coordinates = { Vector3.Zero, Vector3.Zero, Vector3.Zero, Vector3.Zero }; @@ -21,7 +21,7 @@ public partial class SpatialTransformMatrixDialog : Form internal bool ApplySpatialTransform { get; private set; } - internal SpatialTransformMatrixDialog(IObservable> positionDataSource) + internal SpatialTransformMatrixDialog(IObservable positionDataSource) { InitializeComponent(); PositionDataSource = positionDataSource; @@ -41,7 +41,7 @@ private void buttonMeasure_Click(object sender, EventArgs e) buttonCalculate.Enabled = false; var sharedPositionDataGroups = PositionDataSource.Take(NumMeasurements) - .GroupBy(dataFrame => dataFrame.Item1, dataFrame => dataFrame.Item2) + .GroupBy(dataFrame => dataFrame.SensorIndex, dataFrame => dataFrame.Position) .Publish(); sharedPositionDataGroups @@ -136,5 +136,6 @@ private void checkBoxApplySpatialTransform_CheckedChanged(object sender, EventAr { ApplySpatialTransform = checkBoxApplySpatialTransform.Checked; } + } } diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.resx b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.resx index b6de2ecd..edf745a3 100644 --- a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.resx +++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.resx @@ -118,7 +118,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Follow the instructions below to transform naive TS4231 position data from a naive reference frame to a user-defined reference frame. For more in-depth instructions, find the corresponding tutorial in Open Ephys' online documentation. + Follow the instructions below to transform naive TS4231 position data from the base-station reference frame to a user-defined reference frame. For more in-depth instructions, find the corresponding tutorial in Open Ephys' online documentation. 1) Make sure the workflow is running. 2) For each coordinate: • Place the TS4231V1 device and click the corresponding "Measure" button. diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs b/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs index 2503a1a7..2affef22 100644 --- a/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs +++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs @@ -18,7 +18,7 @@ namespace OpenEphys.Onix1.Design public class SpatialTransformMatrixEditor : DataSourceTypeEditor { public SpatialTransformMatrixEditor() - : base(DataSource.Input, typeof(void)) + : base(DataSource.Output, typeof(void)) { } @@ -28,9 +28,9 @@ public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext contex return UITypeEditorEditStyle.Modal; } - protected virtual IObservable> GetData(IObservable> source) + protected virtual IObservable GetData(IObservable> source) { - return source.Merge().Select(coordinate => (Tuple)coordinate); + return source.Merge().Select(x => x as TS4231V1PositionDataFrame); } public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value) diff --git a/OpenEphys.Onix1/SpatialTransform.cs b/OpenEphys.Onix1/SpatialTransform.cs deleted file mode 100644 index 49dab035..00000000 --- a/OpenEphys.Onix1/SpatialTransform.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Bonsai; -using System; -using System.ComponentModel; -using System.Linq; -using System.Reactive.Linq; -using System.Numerics; - -namespace OpenEphys.Onix1 -{ - /// - /// Represents an operator that groups the elements of an observable - /// sequence according to the specified key. - /// - [DefaultProperty(nameof(SpatialTransformMatrix))] - [Description("Transforms 3D coordinates from one reference frame to another.")] - public class SpatialTransform : Transform, Vector3> - { - /// - /// Gets or sets a value specifying the inner properties used as key for - /// each element in the sequence. - /// - [Description("Spatial Transform Matrix")] - [Editor("OpenEphys.Onix1.Design.SpatialTransformMatrixEditor, OpenEphys.Onix1.Design", DesignTypes.UITypeEditor)] - [TypeConverter(typeof(NumericRecordConverter))] - public Matrix4x4 SpatialTransformMatrix { get; set; } = new Matrix4x4( - 1, 0, 0, 0, - 0, 1, 0, 0, - 0, 0, 1, 0, - 0, 0, 0, 1); - - public override IObservable Process(IObservable> source) - { - return source.Select(input => Vector3.Transform(input.Item2, this.SpatialTransformMatrix)); - } - } -} diff --git a/OpenEphys.Onix1/TS4231V1PositionData.cs b/OpenEphys.Onix1/TS4231V1PositionData.cs index d9038d8b..e8a34e33 100644 --- a/OpenEphys.Onix1/TS4231V1PositionData.cs +++ b/OpenEphys.Onix1/TS4231V1PositionData.cs @@ -1,6 +1,7 @@ using System; using System.ComponentModel; using System.Linq; +using System.Numerics; using System.Reactive; using System.Reactive.Linq; using Bonsai; @@ -41,6 +42,7 @@ namespace OpenEphys.Onix1 /// using downstream processing. /// /// + [DefaultProperty(nameof(M))] [Description("Produces a sequence of 3D positions from an array of Triad Semiconductor TS4231 receivers beneath a pair of SteamVR V1 base stations.")] public class TS4231V1PositionData : Source { @@ -73,6 +75,20 @@ public class TS4231V1PositionData : Source [Category(DeviceFactory.ConfigurationCategory)] public Point3d Q { get; set; } = new(1, 0, 0); + + /// + /// Gets or sets a spatial transform to convert position measurements to an external coordinate + /// system. + /// + [Description("Spatial transform matrix to convert position measurements to an external coordinate system.")] + [Category(DeviceFactory.ConfigurationCategory)] + [Editor("OpenEphys.Onix1.Design.SpatialTransformMatrixEditor, OpenEphys.Onix1.Design", DesignTypes.UITypeEditor)] + [TypeConverter(typeof(NumericRecordConverter))] + public Matrix4x4 M { get; set; } = new Matrix4x4( 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1); + /// /// Generates a sequence of objects, each of which contains /// the 3D position of single photodiode. @@ -80,6 +96,7 @@ public class TS4231V1PositionData : Source /// A sequence of objects. public unsafe override IObservable Generate() { + return DeviceManager.GetDevice(DeviceName).SelectMany( deviceInfo => Observable.Create(observer => { @@ -101,7 +118,8 @@ public unsafe override IObservable Generate() return deviceInfo.Context .GetDeviceFrames(device.Address) .SubscribeSafe(frameObserver); - })); + })) + .Select(input => new TS4231V1PositionDataFrame(input.Clock, input.HubClock, input.SensorIndex, Vector3.Transform(input.Position, M))); } } } From de03eac5adbc86ab0773192f528feeeee30284e8 Mon Sep 17 00:00:00 2001 From: cjsha Date: Tue, 22 Apr 2025 20:04:28 -0400 Subject: [PATCH 05/17] Address jpn feedback - add cancel button - add timeout (probably only need timeout or cancel button) - check workflow is running before opening GUI - leave text boxes blank - correctly populate ts4231V1CoordinatesMatrix - give persistent scope to subscriptions so they can be disposed - simplify conditional statement for checking if user input is valid - minor edit to top-level label to be consistent with changes --- .../SpatialTransformMatrixDialog.Designer.cs | 22 +-- .../SpatialTransformMatrixDialog.cs | 161 +++++++++++------- .../SpatialTransformMatrixDialog.resx | 7 +- .../SpatialTransformMatrixEditor.cs | 9 +- 4 files changed, 115 insertions(+), 84 deletions(-) diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.Designer.cs b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.Designer.cs index eea32388..76b94601 100644 --- a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.Designer.cs +++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.Designer.cs @@ -89,9 +89,9 @@ private void InitializeComponent() // this.groupBoxStatus.Controls.Add(this.textBoxStatus); this.groupBoxStatus.Dock = System.Windows.Forms.DockStyle.Fill; - this.groupBoxStatus.Location = new System.Drawing.Point(3, 263); + this.groupBoxStatus.Location = new System.Drawing.Point(3, 250); this.groupBoxStatus.Name = "groupBoxStatus"; - this.groupBoxStatus.Size = new System.Drawing.Size(618, 330); + this.groupBoxStatus.Size = new System.Drawing.Size(618, 343); this.groupBoxStatus.TabIndex = 6; this.groupBoxStatus.TabStop = false; this.groupBoxStatus.Text = "Status Messages"; @@ -106,7 +106,7 @@ private void InitializeComponent() this.textBoxStatus.Name = "textBoxStatus"; this.textBoxStatus.ReadOnly = true; this.textBoxStatus.ScrollBars = System.Windows.Forms.ScrollBars.Vertical; - this.textBoxStatus.Size = new System.Drawing.Size(612, 311); + this.textBoxStatus.Size = new System.Drawing.Size(612, 324); this.textBoxStatus.TabIndex = 3; this.textBoxStatus.Text = "Awaiting user input...\r\n"; // @@ -117,7 +117,7 @@ private void InitializeComponent() this.labelInstructions.Location = new System.Drawing.Point(3, 0); this.labelInstructions.MaximumSize = new System.Drawing.Size(620, 0); this.labelInstructions.Name = "labelInstructions"; - this.labelInstructions.Size = new System.Drawing.Size(618, 104); + this.labelInstructions.Size = new System.Drawing.Size(618, 91); this.labelInstructions.TabIndex = 4; this.labelInstructions.Text = resources.GetString("labelInstructions.Text"); // @@ -147,7 +147,7 @@ private void InitializeComponent() this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxTS4231Coordinate0, 2, 1); this.tableLayoutPanelCoordinates.Controls.Add(this.labelHeaderUser, 3, 0); this.tableLayoutPanelCoordinates.Dock = System.Windows.Forms.DockStyle.Fill; - this.tableLayoutPanelCoordinates.Location = new System.Drawing.Point(3, 107); + this.tableLayoutPanelCoordinates.Location = new System.Drawing.Point(3, 94); this.tableLayoutPanelCoordinates.Name = "tableLayoutPanelCoordinates"; this.tableLayoutPanelCoordinates.RowCount = 5; this.tableLayoutPanelCoordinates.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 30F)); @@ -168,7 +168,6 @@ private void InitializeComponent() this.textBoxUserCoordinate3.Size = new System.Drawing.Size(220, 20); this.textBoxUserCoordinate3.TabIndex = 39; this.textBoxUserCoordinate3.Tag = "7"; - this.textBoxUserCoordinate3.Text = "x\'3, y\'3, z\'3"; this.textBoxUserCoordinate3.TextChanged += new System.EventHandler(this.textBoxUserCoordinate_TextChanged); // // textBoxUserCoordinate2 @@ -180,7 +179,6 @@ private void InitializeComponent() this.textBoxUserCoordinate2.Size = new System.Drawing.Size(220, 20); this.textBoxUserCoordinate2.TabIndex = 38; this.textBoxUserCoordinate2.Tag = "6"; - this.textBoxUserCoordinate2.Text = "x\'2, y\'2, z\'2"; this.textBoxUserCoordinate2.TextChanged += new System.EventHandler(this.textBoxUserCoordinate_TextChanged); // // textBoxUserCoordinate1 @@ -192,7 +190,6 @@ private void InitializeComponent() this.textBoxUserCoordinate1.Size = new System.Drawing.Size(220, 20); this.textBoxUserCoordinate1.TabIndex = 37; this.textBoxUserCoordinate1.Tag = "5"; - this.textBoxUserCoordinate1.Text = "x\'1, y\'1, z\'1"; this.textBoxUserCoordinate1.TextChanged += new System.EventHandler(this.textBoxUserCoordinate_TextChanged); // // textBoxUserCoordinate0 @@ -204,7 +201,6 @@ private void InitializeComponent() this.textBoxUserCoordinate0.Size = new System.Drawing.Size(220, 20); this.textBoxUserCoordinate0.TabIndex = 36; this.textBoxUserCoordinate0.Tag = "4"; - this.textBoxUserCoordinate0.Text = "x\'0, y\'0, z\'0"; this.textBoxUserCoordinate0.TextChanged += new System.EventHandler(this.textBoxUserCoordinate_TextChanged); // // textBoxTS4231Coordinate3 @@ -218,8 +214,6 @@ private void InitializeComponent() this.textBoxTS4231Coordinate3.TabIndex = 33; this.textBoxTS4231Coordinate3.TabStop = false; this.textBoxTS4231Coordinate3.Tag = "3"; - this.textBoxTS4231Coordinate3.Text = "x3, y3, z3"; - this.textBoxTS4231Coordinate3.TextChanged += new System.EventHandler(this.textBoxTS4231Coordinate_TextChanged); // // textBoxTS4231Coordinate2 // @@ -232,8 +226,6 @@ private void InitializeComponent() this.textBoxTS4231Coordinate2.TabIndex = 32; this.textBoxTS4231Coordinate2.TabStop = false; this.textBoxTS4231Coordinate2.Tag = "2"; - this.textBoxTS4231Coordinate2.Text = "x2, y2, z2"; - this.textBoxTS4231Coordinate2.TextChanged += new System.EventHandler(this.textBoxTS4231Coordinate_TextChanged); // // textBoxTS4231Coordinate1 // @@ -246,8 +238,6 @@ private void InitializeComponent() this.textBoxTS4231Coordinate1.TabIndex = 31; this.textBoxTS4231Coordinate1.TabStop = false; this.textBoxTS4231Coordinate1.Tag = "1"; - this.textBoxTS4231Coordinate1.Text = "x1, y1, z1"; - this.textBoxTS4231Coordinate1.TextChanged += new System.EventHandler(this.textBoxTS4231Coordinate_TextChanged); // // buttonMeasure3 // @@ -355,8 +345,6 @@ private void InitializeComponent() this.textBoxTS4231Coordinate0.TabIndex = 30; this.textBoxTS4231Coordinate0.TabStop = false; this.textBoxTS4231Coordinate0.Tag = "0"; - this.textBoxTS4231Coordinate0.Text = "x0, y0, z0"; - this.textBoxTS4231Coordinate0.TextChanged += new System.EventHandler(this.textBoxTS4231Coordinate_TextChanged); // // labelHeaderUser // diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs index d77e554b..0b445993 100644 --- a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs +++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs @@ -17,7 +17,13 @@ public partial class SpatialTransformMatrixDialog : Form private Vector3[] TS4231Coordinates = { Vector3.Zero, Vector3.Zero, Vector3.Zero, Vector3.Zero }; - internal Matrix4x4 SpatialTransform { get; private set; } + private Button[] MeasureButtons; + + private IDisposable TextBoxStatusUpdateSubscription; + + private IDisposable MeasurementCalculationSubscription; + + internal Matrix4x4 NewSpatialTransform { get; private set; } internal bool ApplySpatialTransform { get; private set; } @@ -25,75 +31,108 @@ internal SpatialTransformMatrixDialog(IObservable pos { InitializeComponent(); PositionDataSource = positionDataSource; + MeasureButtons = new Button[] { buttonMeasure0, buttonMeasure1, buttonMeasure2, buttonMeasure3 }; + } + + private void enableButtons(bool enable, byte index) + { + for (byte i = 0; i < MeasureButtons.Length; i++) + { + MeasureButtons[i].Enabled = enable || (i == index); + } + buttonCalculate.Enabled = enable && InputsValid.All(inputValid => inputValid); } private void buttonMeasure_Click(object sender, EventArgs e) { TextBox[] ts4231TextBoxes = { textBoxTS4231Coordinate0, textBoxTS4231Coordinate1, textBoxTS4231Coordinate2, textBoxTS4231Coordinate3 }; - var index = int.Parse((string)((Button)sender).Tag); - - textBoxStatus.AppendText(string.Format("Measuring coordinate {0}...", index) + Environment.NewLine); - - buttonMeasure0.Enabled = false; - buttonMeasure1.Enabled = false; - buttonMeasure2.Enabled = false; - buttonMeasure3.Enabled = false; - buttonCalculate.Enabled = false; - - var sharedPositionDataGroups = PositionDataSource.Take(NumMeasurements) - .GroupBy(dataFrame => dataFrame.SensorIndex, dataFrame => dataFrame.Position) - .Publish(); - - sharedPositionDataGroups - .SelectMany(group => group.Count().Select(count => new { index = group.Key, measurementCount = count })) - .ObserveOn(new ControlScheduler(this)) - .Finally(() => - { - textBoxStatus.AppendText(string.Format("Measurements at coordinate {0} complete.", index) - + Environment.NewLine + Environment.NewLine + "Awaiting user input..." + Environment.NewLine); - buttonMeasure0.Enabled = true; - buttonMeasure1.Enabled = true; - buttonMeasure2.Enabled = true; - buttonMeasure3.Enabled = true; - buttonCalculate.Enabled = InputsValid.All(inputValid => inputValid); - }) - .Subscribe(sensor => - { - textBoxStatus.AppendText(string.Format("{1} measurements from sensor {0}.", sensor.index, sensor.measurementCount) + Environment.NewLine); - }); - - sharedPositionDataGroups - .Merge() - .ObserveOn(new ControlScheduler(this)) - .Aggregate( - new Vector3(0, 0, 0), - (acc, current) => acc + current, - acc => + var index = byte.Parse((string)((Button)sender).Tag); + + if (MeasureButtons[index].Text == "Measure") + { + textBoxStatus.AppendText(string.Format("Measuring coordinate {0}...", index) + Environment.NewLine); + MeasureButtons[index].Text = "Cancel"; + enableButtons(false, index); + + var sharedPositionDataGroups = PositionDataSource + .Take(NumMeasurements) + .Timeout(new TimeSpan(0, 0, 5), Observable.Empty()) + .Publish(); + + TextBoxStatusUpdateSubscription = sharedPositionDataGroups + .GroupBy(dataFrame => dataFrame.SensorIndex, dataFrame => dataFrame.Position) + .SelectMany(group => group.Count().Select(count => new { Index = group.Key, MeasurementCount = count })) + .Aggregate( + (TextBoxStatusUpdate: "", Count: 0), + (acc, sensor) => + { + var textBoxStatusUpdateString = acc.TextBoxStatusUpdate; + textBoxStatusUpdateString += string.Format("{0} measurements from sensor {1}.", + sensor.MeasurementCount, sensor.Index); + textBoxStatusUpdateString += Environment.NewLine; + return (textBoxStatusUpdateString, acc.Count + sensor.MeasurementCount); + }, + acc => (TextBoxStatusUpdate: acc.TextBoxStatusUpdate, Valid: acc.Count == NumMeasurements)) + .ObserveOn(new ControlScheduler(this)) + .Subscribe(finalResult => { - TS4231Coordinates[index] = acc / NumMeasurements; - ts4231TextBoxes[index].Text = string.Format("{0}, {1}, {2}", - TS4231Coordinates[index].X, - TS4231Coordinates[index].Y, - TS4231Coordinates[index].Z); - return TS4231Coordinates[index]; - }) - .Subscribe(); - - sharedPositionDataGroups.Connect(); - } - - private void textBoxTS4231Coordinate_TextChanged(object sender, EventArgs e) - { - var index = int.Parse((string)((TextBox)sender).Tag); - InputsValid[index] = true; - buttonCalculate.Enabled = InputsValid.All(inputValid => inputValid); + if (finalResult.Valid) + { + textBoxStatus.AppendText(finalResult.TextBoxStatusUpdate); + textBoxStatus.AppendText(string.Format("Measurements at coordinate {0} complete.", index) + + Environment.NewLine + Environment.NewLine + "Awaiting user input..." + Environment.NewLine); + } + else + { + textBoxStatus.AppendText(string.Format("Measurements at coordinate {0} timed out. ", index) + + "Confirm the Lighthouse receivers are within range and unobstructed from Lighthouse transmitters." + + Environment.NewLine + Environment.NewLine + "Awaiting user input..." + Environment.NewLine); + } + enableButtons(true, index); + }); + + MeasurementCalculationSubscription = sharedPositionDataGroups + .Aggregate( + (Sum: Vector3.Zero, Count: 0), + (acc, current) => (acc.Sum + current.Position, acc.Count + 1), + acc => + { + TS4231Coordinates[index] = acc.Item1 / NumMeasurements; + return (Position: TS4231Coordinates[index], Valid: acc.Count == NumMeasurements); + }) + .ObserveOn(new ControlScheduler(this)) + .Subscribe(finalMeasurement => + { + MeasureButtons[index].Text = "Measure"; + if (finalMeasurement.Valid) + { + ts4231TextBoxes[index].Text = string.Format("{0}, {1}, {2}", + finalMeasurement.Position.X, + finalMeasurement.Position.Y, + finalMeasurement.Position.Z); + InputsValid[index] = true; + buttonCalculate.Enabled = InputsValid.All(inputValid => inputValid); + } + }); + + sharedPositionDataGroups.Connect(); + } + else + { + TextBoxStatusUpdateSubscription.Dispose(); + MeasurementCalculationSubscription.Dispose(); + textBoxStatus.AppendText(string.Format("Measurements at coordinate {0} cancelled by user.", index) + + Environment.NewLine + Environment.NewLine + "Awaiting user input..." + Environment.NewLine); + MeasureButtons[index].Text = "Measure"; + enableButtons(true, index); + } } private void textBoxUserCoordinate_TextChanged(object sender, EventArgs e) { var index = int.Parse((string)((TextBox)sender).Tag); string[] serInputSplit = ((TextBox)sender).Text.Split(','); - InputsValid[index] = serInputSplit.Length == 3 ? serInputSplit.All(floatCandidate => float.TryParse(floatCandidate, out _)) : false; + InputsValid[index] = serInputSplit.Length == 3 && serInputSplit.All(floatCandidate => float.TryParse(floatCandidate, out _)); buttonCalculate.Enabled = InputsValid.All(inputValid => inputValid); } @@ -101,7 +140,7 @@ private void buttonCalculate_Click(object sender, EventArgs e) { var ts4231V1CoordinatesMatrix = new Matrix4x4( TS4231Coordinates[0].X, TS4231Coordinates[0].Y, TS4231Coordinates[0].Z, 1, - TS4231Coordinates[1].X, TS4231Coordinates[1].Y, TS4231Coordinates[1].Y, 1, + TS4231Coordinates[1].X, TS4231Coordinates[1].Y, TS4231Coordinates[1].Z, 1, TS4231Coordinates[2].X, TS4231Coordinates[2].Y, TS4231Coordinates[2].Z, 1, TS4231Coordinates[3].X, TS4231Coordinates[3].Y, TS4231Coordinates[3].Z, 1); @@ -118,10 +157,10 @@ private void buttonCalculate_Click(object sender, EventArgs e) userCoordinates[3][0], userCoordinates[3][1], userCoordinates[3][2], 1); Matrix4x4.Invert(ts4231V1CoordinatesMatrix, out var ts4231V1CoordinatesMatrixInverted); - SpatialTransform = Matrix4x4.Multiply(ts4231V1CoordinatesMatrixInverted, userCoordinatesMatrix); + NewSpatialTransform = Matrix4x4.Multiply(ts4231V1CoordinatesMatrixInverted, userCoordinatesMatrix); textBoxStatus.AppendText("The spatial transform matrix for the above coordinates is:" + Environment.NewLine); - textBoxStatus.AppendText(SpatialTransform.ToString() + Environment.NewLine + Environment.NewLine); + textBoxStatus.AppendText(NewSpatialTransform.ToString() + Environment.NewLine + Environment.NewLine); textBoxStatus.AppendText("Awaiting user input..." + Environment.NewLine); checkBoxApplySpatialTransform.Enabled = true; diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.resx b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.resx index edf745a3..3791e7ed 100644 --- a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.resx +++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.resx @@ -119,12 +119,11 @@ Follow the instructions below to transform naive TS4231 position data from the base-station reference frame to a user-defined reference frame. For more in-depth instructions, find the corresponding tutorial in Open Ephys' online documentation. -1) Make sure the workflow is running. -2) For each coordinate: +1) For each coordinate: • Place the TS4231V1 device and click the corresponding "Measure" button. • Input how would like to define the coordinate in the user-defined reference frame. -3) Click the "Calculate Spatial Transform" button which enables after all fields are populated with valid data. -4) To automatically set the SpatialTransformMatrix property, check the bottom checkbox and close this GUI. +2) Click the "Calculate Spatial Transform" button which enables after all fields are populated with valid data. +3) To automatically set the SpatialTransformMatrix property, check the bottom checkbox and close this GUI. diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs b/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs index 2affef22..ca978988 100644 --- a/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs +++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs @@ -36,15 +36,20 @@ protected virtual IObservable GetData(IObservable Date: Tue, 6 May 2025 17:49:41 -0400 Subject: [PATCH 06/17] Invert spatial transform of positions entering SpatialTransformMatrixDialog - This should recover the raw position values from P and Q alone without the M that is currently being edited. --- .../SpatialTransformMatrixDialog.cs | 33 ++++++++++--------- .../SpatialTransformMatrixEditor.cs | 2 +- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs index 0b445993..55929a72 100644 --- a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs +++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs @@ -9,27 +9,29 @@ namespace OpenEphys.Onix1.Design { public partial class SpatialTransformMatrixDialog : Form { - private const byte NumMeasurements = 100; + const byte NumMeasurements = 100; + readonly Matrix4x4 inverseM; - private bool[] InputsValid = { false, false, false, false, false, false, false, false }; + readonly bool[] InputsValid = { false, false, false, false, false, false, false, false }; + readonly IObservable PositionDataSource; + readonly Vector3[] TS4231Coordinates = { Vector3.Zero, Vector3.Zero, Vector3.Zero, Vector3.Zero }; + readonly Button[] MeasureButtons; - private IObservable PositionDataSource; - - private Vector3[] TS4231Coordinates = { Vector3.Zero, Vector3.Zero, Vector3.Zero, Vector3.Zero }; - - private Button[] MeasureButtons; - - private IDisposable TextBoxStatusUpdateSubscription; - - private IDisposable MeasurementCalculationSubscription; + IDisposable TextBoxStatusUpdateSubscription; + IDisposable MeasurementCalculationSubscription; internal Matrix4x4 NewSpatialTransform { get; private set; } internal bool ApplySpatialTransform { get; private set; } - internal SpatialTransformMatrixDialog(IObservable positionDataSource) + internal SpatialTransformMatrixDialog(IObservable positionDataSource, Matrix4x4 currentM) { InitializeComponent(); + if (!Matrix4x4.Invert(currentM, out inverseM)) + { + throw new ArgumentException("Current spatial transform matrix is non-invertible. " + + "You can set M to the identity matrix if you want to start anew."); + } PositionDataSource = positionDataSource; MeasureButtons = new Button[] { buttonMeasure0, buttonMeasure1, buttonMeasure2, buttonMeasure3 }; } @@ -72,7 +74,7 @@ private void buttonMeasure_Click(object sender, EventArgs e) textBoxStatusUpdateString += Environment.NewLine; return (textBoxStatusUpdateString, acc.Count + sensor.MeasurementCount); }, - acc => (TextBoxStatusUpdate: acc.TextBoxStatusUpdate, Valid: acc.Count == NumMeasurements)) + acc => (acc.TextBoxStatusUpdate, Valid: acc.Count == NumMeasurements)) .ObserveOn(new ControlScheduler(this)) .Subscribe(finalResult => { @@ -94,10 +96,10 @@ private void buttonMeasure_Click(object sender, EventArgs e) MeasurementCalculationSubscription = sharedPositionDataGroups .Aggregate( (Sum: Vector3.Zero, Count: 0), - (acc, current) => (acc.Sum + current.Position, acc.Count + 1), + (acc, current) => (acc.Sum + Vector3.Transform(current.Position, inverseM), acc.Count + 1), acc => { - TS4231Coordinates[index] = acc.Item1 / NumMeasurements; + TS4231Coordinates[index] = acc.Sum / NumMeasurements; return (Position: TS4231Coordinates[index], Valid: acc.Count == NumMeasurements); }) .ObserveOn(new ControlScheduler(this)) @@ -175,6 +177,5 @@ private void checkBoxApplySpatialTransform_CheckedChanged(object sender, EventAr { ApplySpatialTransform = checkBoxApplySpatialTransform.Checked; } - } } diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs b/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs index ca978988..e4bdf73f 100644 --- a/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs +++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs @@ -41,7 +41,7 @@ public override object EditValue(ITypeDescriptorContext context, IServiceProvide { var source = GetDataSource(context, provider); var dataFrames = GetData(source.Output); - using (var visualizerDialog = new SpatialTransformMatrixDialog(dataFrames)) + using (var visualizerDialog = new SpatialTransformMatrixDialog(dataFrames, (Matrix4x4)value)) { if (!editorState.WorkflowRunning) { From af45a4a1e43ae2c4cd635134867e4e85130e709d Mon Sep 17 00:00:00 2001 From: cjsha Date: Mon, 19 May 2025 19:29:27 -0400 Subject: [PATCH 07/17] Address jonnew's feedback jonnew's feedback: - Add status strip - Use "OK", "Cancel" button paradigms additionally: - Improve resizing - Address all VS messages - PascalCase methods - Make "using" syntax more concise - Add/Edit some XML comments - Instead of transforming every Vector3 and then averaging, average all Vector3s and then take the transform - Inline/remove the GetData() function --- .../SpatialTransformMatrixDialog.Designer.cs | 178 +++++++++++------- .../SpatialTransformMatrixDialog.cs | 52 +++-- .../SpatialTransformMatrixDialog.resx | 12 +- .../SpatialTransformMatrixEditor.cs | 29 ++- 4 files changed, 164 insertions(+), 107 deletions(-) diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.Designer.cs b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.Designer.cs index 76b94601..17e2994e 100644 --- a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.Designer.cs +++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.Designer.cs @@ -56,18 +56,22 @@ private void InitializeComponent() this.labelHeaderUser = new System.Windows.Forms.Label(); this.buttonCalculate = new System.Windows.Forms.Button(); this.flowLayoutPanelBottom = new System.Windows.Forms.FlowLayoutPanel(); - this.buttonClose = new System.Windows.Forms.Button(); - this.checkBoxApplySpatialTransform = new System.Windows.Forms.CheckBox(); + this.buttonCancel = new System.Windows.Forms.Button(); + this.buttonOK = new System.Windows.Forms.Button(); + this.statusStrip = new System.Windows.Forms.StatusStrip(); + this.toolStripStatusLabelTS4231 = new System.Windows.Forms.ToolStripStatusLabel(); + this.toolStripStatusLabelUser = new System.Windows.Forms.ToolStripStatusLabel(); this.tableLayoutPanelMain.SuspendLayout(); this.groupBoxStatus.SuspendLayout(); this.tableLayoutPanelCoordinates.SuspendLayout(); this.flowLayoutPanelBottom.SuspendLayout(); + this.statusStrip.SuspendLayout(); this.SuspendLayout(); // // tableLayoutPanelMain // this.tableLayoutPanelMain.ColumnCount = 1; - this.tableLayoutPanelMain.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); + this.tableLayoutPanelMain.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); this.tableLayoutPanelMain.Controls.Add(this.groupBoxStatus, 0, 2); this.tableLayoutPanelMain.Controls.Add(this.labelInstructions, 0, 0); this.tableLayoutPanelMain.Controls.Add(this.tableLayoutPanelCoordinates, 0, 1); @@ -82,16 +86,16 @@ private void InitializeComponent() this.tableLayoutPanelMain.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F)); this.tableLayoutPanelMain.RowStyles.Add(new System.Windows.Forms.RowStyle()); this.tableLayoutPanelMain.RowStyles.Add(new System.Windows.Forms.RowStyle()); - this.tableLayoutPanelMain.Size = new System.Drawing.Size(624, 661); + this.tableLayoutPanelMain.Size = new System.Drawing.Size(604, 639); this.tableLayoutPanelMain.TabIndex = 7; // // groupBoxStatus // this.groupBoxStatus.Controls.Add(this.textBoxStatus); this.groupBoxStatus.Dock = System.Windows.Forms.DockStyle.Fill; - this.groupBoxStatus.Location = new System.Drawing.Point(3, 250); + this.groupBoxStatus.Location = new System.Drawing.Point(3, 263); this.groupBoxStatus.Name = "groupBoxStatus"; - this.groupBoxStatus.Size = new System.Drawing.Size(618, 343); + this.groupBoxStatus.Size = new System.Drawing.Size(598, 308); this.groupBoxStatus.TabIndex = 6; this.groupBoxStatus.TabStop = false; this.groupBoxStatus.Text = "Status Messages"; @@ -106,23 +110,23 @@ private void InitializeComponent() this.textBoxStatus.Name = "textBoxStatus"; this.textBoxStatus.ReadOnly = true; this.textBoxStatus.ScrollBars = System.Windows.Forms.ScrollBars.Vertical; - this.textBoxStatus.Size = new System.Drawing.Size(612, 324); + this.textBoxStatus.Size = new System.Drawing.Size(592, 289); this.textBoxStatus.TabIndex = 3; this.textBoxStatus.Text = "Awaiting user input...\r\n"; // // labelInstructions // this.labelInstructions.AutoSize = true; - this.labelInstructions.Dock = System.Windows.Forms.DockStyle.Fill; this.labelInstructions.Location = new System.Drawing.Point(3, 0); - this.labelInstructions.MaximumSize = new System.Drawing.Size(620, 0); this.labelInstructions.Name = "labelInstructions"; - this.labelInstructions.Size = new System.Drawing.Size(618, 91); + this.labelInstructions.Size = new System.Drawing.Size(596, 104); this.labelInstructions.TabIndex = 4; this.labelInstructions.Text = resources.GetString("labelInstructions.Text"); // // tableLayoutPanelCoordinates // + this.tableLayoutPanelCoordinates.AutoSize = true; + this.tableLayoutPanelCoordinates.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; this.tableLayoutPanelCoordinates.ColumnCount = 4; this.tableLayoutPanelCoordinates.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); this.tableLayoutPanelCoordinates.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); @@ -147,7 +151,7 @@ private void InitializeComponent() this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxTS4231Coordinate0, 2, 1); this.tableLayoutPanelCoordinates.Controls.Add(this.labelHeaderUser, 3, 0); this.tableLayoutPanelCoordinates.Dock = System.Windows.Forms.DockStyle.Fill; - this.tableLayoutPanelCoordinates.Location = new System.Drawing.Point(3, 94); + this.tableLayoutPanelCoordinates.Location = new System.Drawing.Point(3, 107); this.tableLayoutPanelCoordinates.Name = "tableLayoutPanelCoordinates"; this.tableLayoutPanelCoordinates.RowCount = 5; this.tableLayoutPanelCoordinates.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 30F)); @@ -155,53 +159,53 @@ private void InitializeComponent() this.tableLayoutPanelCoordinates.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 30F)); this.tableLayoutPanelCoordinates.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 30F)); this.tableLayoutPanelCoordinates.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 30F)); - this.tableLayoutPanelCoordinates.Size = new System.Drawing.Size(618, 150); + this.tableLayoutPanelCoordinates.Size = new System.Drawing.Size(598, 150); this.tableLayoutPanelCoordinates.TabIndex = 5; this.tableLayoutPanelCoordinates.Tag = "6"; // // textBoxUserCoordinate3 // this.textBoxUserCoordinate3.Dock = System.Windows.Forms.DockStyle.Fill; - this.textBoxUserCoordinate3.Location = new System.Drawing.Point(395, 123); + this.textBoxUserCoordinate3.Location = new System.Drawing.Point(385, 123); this.textBoxUserCoordinate3.MinimumSize = new System.Drawing.Size(150, 4); this.textBoxUserCoordinate3.Name = "textBoxUserCoordinate3"; - this.textBoxUserCoordinate3.Size = new System.Drawing.Size(220, 20); + this.textBoxUserCoordinate3.Size = new System.Drawing.Size(210, 20); this.textBoxUserCoordinate3.TabIndex = 39; this.textBoxUserCoordinate3.Tag = "7"; - this.textBoxUserCoordinate3.TextChanged += new System.EventHandler(this.textBoxUserCoordinate_TextChanged); + this.textBoxUserCoordinate3.TextChanged += new System.EventHandler(this.TextBoxUserCoordinate_TextChanged); // // textBoxUserCoordinate2 // this.textBoxUserCoordinate2.Dock = System.Windows.Forms.DockStyle.Fill; - this.textBoxUserCoordinate2.Location = new System.Drawing.Point(395, 93); + this.textBoxUserCoordinate2.Location = new System.Drawing.Point(385, 93); this.textBoxUserCoordinate2.MinimumSize = new System.Drawing.Size(150, 4); this.textBoxUserCoordinate2.Name = "textBoxUserCoordinate2"; - this.textBoxUserCoordinate2.Size = new System.Drawing.Size(220, 20); + this.textBoxUserCoordinate2.Size = new System.Drawing.Size(210, 20); this.textBoxUserCoordinate2.TabIndex = 38; this.textBoxUserCoordinate2.Tag = "6"; - this.textBoxUserCoordinate2.TextChanged += new System.EventHandler(this.textBoxUserCoordinate_TextChanged); + this.textBoxUserCoordinate2.TextChanged += new System.EventHandler(this.TextBoxUserCoordinate_TextChanged); // // textBoxUserCoordinate1 // this.textBoxUserCoordinate1.Dock = System.Windows.Forms.DockStyle.Fill; - this.textBoxUserCoordinate1.Location = new System.Drawing.Point(395, 63); + this.textBoxUserCoordinate1.Location = new System.Drawing.Point(385, 63); this.textBoxUserCoordinate1.MinimumSize = new System.Drawing.Size(150, 4); this.textBoxUserCoordinate1.Name = "textBoxUserCoordinate1"; - this.textBoxUserCoordinate1.Size = new System.Drawing.Size(220, 20); + this.textBoxUserCoordinate1.Size = new System.Drawing.Size(210, 20); this.textBoxUserCoordinate1.TabIndex = 37; this.textBoxUserCoordinate1.Tag = "5"; - this.textBoxUserCoordinate1.TextChanged += new System.EventHandler(this.textBoxUserCoordinate_TextChanged); + this.textBoxUserCoordinate1.TextChanged += new System.EventHandler(this.TextBoxUserCoordinate_TextChanged); // // textBoxUserCoordinate0 // this.textBoxUserCoordinate0.Dock = System.Windows.Forms.DockStyle.Fill; - this.textBoxUserCoordinate0.Location = new System.Drawing.Point(395, 33); + this.textBoxUserCoordinate0.Location = new System.Drawing.Point(385, 33); this.textBoxUserCoordinate0.MinimumSize = new System.Drawing.Size(150, 4); this.textBoxUserCoordinate0.Name = "textBoxUserCoordinate0"; - this.textBoxUserCoordinate0.Size = new System.Drawing.Size(220, 20); + this.textBoxUserCoordinate0.Size = new System.Drawing.Size(210, 20); this.textBoxUserCoordinate0.TabIndex = 36; this.textBoxUserCoordinate0.Tag = "4"; - this.textBoxUserCoordinate0.TextChanged += new System.EventHandler(this.textBoxUserCoordinate_TextChanged); + this.textBoxUserCoordinate0.TextChanged += new System.EventHandler(this.TextBoxUserCoordinate_TextChanged); // // textBoxTS4231Coordinate3 // @@ -210,7 +214,7 @@ private void InitializeComponent() this.textBoxTS4231Coordinate3.MinimumSize = new System.Drawing.Size(150, 4); this.textBoxTS4231Coordinate3.Name = "textBoxTS4231Coordinate3"; this.textBoxTS4231Coordinate3.ReadOnly = true; - this.textBoxTS4231Coordinate3.Size = new System.Drawing.Size(220, 20); + this.textBoxTS4231Coordinate3.Size = new System.Drawing.Size(210, 20); this.textBoxTS4231Coordinate3.TabIndex = 33; this.textBoxTS4231Coordinate3.TabStop = false; this.textBoxTS4231Coordinate3.Tag = "3"; @@ -222,7 +226,7 @@ private void InitializeComponent() this.textBoxTS4231Coordinate2.MinimumSize = new System.Drawing.Size(150, 4); this.textBoxTS4231Coordinate2.Name = "textBoxTS4231Coordinate2"; this.textBoxTS4231Coordinate2.ReadOnly = true; - this.textBoxTS4231Coordinate2.Size = new System.Drawing.Size(220, 20); + this.textBoxTS4231Coordinate2.Size = new System.Drawing.Size(210, 20); this.textBoxTS4231Coordinate2.TabIndex = 32; this.textBoxTS4231Coordinate2.TabStop = false; this.textBoxTS4231Coordinate2.Tag = "2"; @@ -234,7 +238,7 @@ private void InitializeComponent() this.textBoxTS4231Coordinate1.MinimumSize = new System.Drawing.Size(150, 4); this.textBoxTS4231Coordinate1.Name = "textBoxTS4231Coordinate1"; this.textBoxTS4231Coordinate1.ReadOnly = true; - this.textBoxTS4231Coordinate1.Size = new System.Drawing.Size(220, 20); + this.textBoxTS4231Coordinate1.Size = new System.Drawing.Size(210, 20); this.textBoxTS4231Coordinate1.TabIndex = 31; this.textBoxTS4231Coordinate1.TabStop = false; this.textBoxTS4231Coordinate1.Tag = "1"; @@ -248,7 +252,7 @@ private void InitializeComponent() this.buttonMeasure3.Tag = "3"; this.buttonMeasure3.Text = "Measure"; this.buttonMeasure3.UseVisualStyleBackColor = true; - this.buttonMeasure3.Click += new System.EventHandler(this.buttonMeasure_Click); + this.buttonMeasure3.Click += new System.EventHandler(this.ButtonMeasure_Click); // // buttonMeasure2 // @@ -259,7 +263,7 @@ private void InitializeComponent() this.buttonMeasure2.Tag = "2"; this.buttonMeasure2.Text = "Measure"; this.buttonMeasure2.UseVisualStyleBackColor = true; - this.buttonMeasure2.Click += new System.EventHandler(this.buttonMeasure_Click); + this.buttonMeasure2.Click += new System.EventHandler(this.ButtonMeasure_Click); // // buttonMeasure1 // @@ -271,7 +275,7 @@ private void InitializeComponent() this.buttonMeasure1.Tag = "1"; this.buttonMeasure1.Text = "Measure"; this.buttonMeasure1.UseVisualStyleBackColor = true; - this.buttonMeasure1.Click += new System.EventHandler(this.buttonMeasure_Click); + this.buttonMeasure1.Click += new System.EventHandler(this.ButtonMeasure_Click); // // labelCoordinate3 // @@ -311,7 +315,7 @@ private void InitializeComponent() this.buttonMeasure0.Tag = "0"; this.buttonMeasure0.Text = "Measure"; this.buttonMeasure0.UseVisualStyleBackColor = true; - this.buttonMeasure0.Click += new System.EventHandler(this.buttonMeasure_Click); + this.buttonMeasure0.Click += new System.EventHandler(this.ButtonMeasure_Click); // // labelHeaderTS4231 // @@ -320,7 +324,7 @@ private void InitializeComponent() this.labelHeaderTS4231.Location = new System.Drawing.Point(80, 0); this.labelHeaderTS4231.Margin = new System.Windows.Forms.Padding(0); this.labelHeaderTS4231.Name = "labelHeaderTS4231"; - this.labelHeaderTS4231.Size = new System.Drawing.Size(312, 30); + this.labelHeaderTS4231.Size = new System.Drawing.Size(302, 30); this.labelHeaderTS4231.TabIndex = 0; this.labelHeaderTS4231.Text = "Naive TS4231 Coordinates"; this.labelHeaderTS4231.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; @@ -341,7 +345,7 @@ private void InitializeComponent() this.textBoxTS4231Coordinate0.MinimumSize = new System.Drawing.Size(150, 4); this.textBoxTS4231Coordinate0.Name = "textBoxTS4231Coordinate0"; this.textBoxTS4231Coordinate0.ReadOnly = true; - this.textBoxTS4231Coordinate0.Size = new System.Drawing.Size(220, 20); + this.textBoxTS4231Coordinate0.Size = new System.Drawing.Size(210, 20); this.textBoxTS4231Coordinate0.TabIndex = 30; this.textBoxTS4231Coordinate0.TabStop = false; this.textBoxTS4231Coordinate0.Tag = "0"; @@ -349,10 +353,10 @@ private void InitializeComponent() // labelHeaderUser // this.labelHeaderUser.Dock = System.Windows.Forms.DockStyle.Fill; - this.labelHeaderUser.Location = new System.Drawing.Point(395, 0); + this.labelHeaderUser.Location = new System.Drawing.Point(385, 0); this.labelHeaderUser.MinimumSize = new System.Drawing.Size(150, 0); this.labelHeaderUser.Name = "labelHeaderUser"; - this.labelHeaderUser.Size = new System.Drawing.Size(220, 30); + this.labelHeaderUser.Size = new System.Drawing.Size(210, 30); this.labelHeaderUser.TabIndex = 34; this.labelHeaderUser.Text = "User-Defined Coordinates"; this.labelHeaderUser.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; @@ -361,58 +365,87 @@ private void InitializeComponent() // this.buttonCalculate.Dock = System.Windows.Forms.DockStyle.Fill; this.buttonCalculate.Enabled = false; - this.buttonCalculate.Location = new System.Drawing.Point(3, 599); + this.buttonCalculate.Location = new System.Drawing.Point(3, 577); this.buttonCalculate.Name = "buttonCalculate"; - this.buttonCalculate.Size = new System.Drawing.Size(618, 23); + this.buttonCalculate.Size = new System.Drawing.Size(598, 23); this.buttonCalculate.TabIndex = 7; this.buttonCalculate.Text = "Calculate Spatial Transform"; this.buttonCalculate.UseVisualStyleBackColor = true; - this.buttonCalculate.Click += new System.EventHandler(this.buttonCalculate_Click); + this.buttonCalculate.Click += new System.EventHandler(this.ButtonCalculate_Click); // // flowLayoutPanelBottom // this.flowLayoutPanelBottom.AutoSize = true; - this.flowLayoutPanelBottom.Controls.Add(this.buttonClose); - this.flowLayoutPanelBottom.Controls.Add(this.checkBoxApplySpatialTransform); + this.flowLayoutPanelBottom.Controls.Add(this.buttonCancel); + this.flowLayoutPanelBottom.Controls.Add(this.buttonOK); this.flowLayoutPanelBottom.Dock = System.Windows.Forms.DockStyle.Fill; this.flowLayoutPanelBottom.FlowDirection = System.Windows.Forms.FlowDirection.RightToLeft; - this.flowLayoutPanelBottom.Location = new System.Drawing.Point(3, 628); + this.flowLayoutPanelBottom.Location = new System.Drawing.Point(3, 606); this.flowLayoutPanelBottom.Name = "flowLayoutPanelBottom"; - this.flowLayoutPanelBottom.Size = new System.Drawing.Size(618, 30); + this.flowLayoutPanelBottom.Size = new System.Drawing.Size(598, 30); this.flowLayoutPanelBottom.TabIndex = 8; // - // buttonClose - // - this.buttonClose.DialogResult = System.Windows.Forms.DialogResult.OK; - this.buttonClose.Location = new System.Drawing.Point(535, 3); - this.buttonClose.Name = "buttonClose"; - this.buttonClose.Size = new System.Drawing.Size(80, 24); - this.buttonClose.TabIndex = 0; - this.buttonClose.Text = "Close"; - this.buttonClose.UseVisualStyleBackColor = true; - this.buttonClose.Click += new System.EventHandler(this.buttonClose_Click); - // - // checkBoxApplySpatialTransform - // - this.checkBoxApplySpatialTransform.AutoSize = true; - this.checkBoxApplySpatialTransform.Dock = System.Windows.Forms.DockStyle.Fill; - this.checkBoxApplySpatialTransform.Enabled = false; - this.checkBoxApplySpatialTransform.Location = new System.Drawing.Point(268, 3); - this.checkBoxApplySpatialTransform.Name = "checkBoxApplySpatialTransform"; - this.checkBoxApplySpatialTransform.Size = new System.Drawing.Size(261, 24); - this.checkBoxApplySpatialTransform.TabIndex = 1; - this.checkBoxApplySpatialTransform.Text = "Set SpatialTransformMatrix property when closing."; - this.checkBoxApplySpatialTransform.UseVisualStyleBackColor = true; - this.checkBoxApplySpatialTransform.CheckedChanged += new System.EventHandler(this.checkBoxApplySpatialTransform_CheckedChanged); + // buttonCancel + // + this.buttonCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel; + this.buttonCancel.Location = new System.Drawing.Point(515, 3); + this.buttonCancel.Name = "buttonCancel"; + this.buttonCancel.Size = new System.Drawing.Size(80, 24); + this.buttonCancel.TabIndex = 0; + this.buttonCancel.Text = "Cancel"; + this.buttonCancel.UseVisualStyleBackColor = true; + this.buttonCancel.Click += new System.EventHandler(this.ButtonOKOrCancel_Click); + // + // buttonOK + // + this.buttonOK.DialogResult = System.Windows.Forms.DialogResult.OK; + this.buttonOK.Enabled = false; + this.buttonOK.Location = new System.Drawing.Point(429, 3); + this.buttonOK.Name = "buttonOK"; + this.buttonOK.Size = new System.Drawing.Size(80, 24); + this.buttonOK.TabIndex = 2; + this.buttonOK.Text = "OK"; + this.buttonOK.UseVisualStyleBackColor = true; + this.buttonOK.Click += new System.EventHandler(this.ButtonOKOrCancel_Click); + // + // statusStrip + // + this.statusStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.toolStripStatusLabelTS4231, + this.toolStripStatusLabelUser}); + this.statusStrip.Location = new System.Drawing.Point(0, 639); + this.statusStrip.Name = "statusStrip"; + this.statusStrip.ShowItemToolTips = true; + this.statusStrip.Size = new System.Drawing.Size(604, 22); + this.statusStrip.TabIndex = 8; + this.statusStrip.Text = "statusStrip1"; + // + // toolStripStatusLabelTS4231 + // + this.toolStripStatusLabelTS4231.Image = global::OpenEphys.Onix1.Design.Properties.Resources.StatusBlockedImage; + this.toolStripStatusLabelTS4231.Name = "toolStripStatusLabelTS4231"; + this.toolStripStatusLabelTS4231.Size = new System.Drawing.Size(237, 17); + this.toolStripStatusLabelTS4231.Text = "At least one TS4231 coordinate is invalid."; + this.toolStripStatusLabelTS4231.ToolTipText = "All four TS4231 coordinates must be measured before the spatial transform matrix " + + "can be calculated."; + // + // toolStripStatusLabelUser + // + this.toolStripStatusLabelUser.Image = global::OpenEphys.Onix1.Design.Properties.Resources.StatusBlockedImage; + this.toolStripStatusLabelUser.Name = "toolStripStatusLabelUser"; + this.toolStripStatusLabelUser.Size = new System.Drawing.Size(267, 17); + this.toolStripStatusLabelUser.Text = "At least one user-defined coordinate is invalid."; + this.toolStripStatusLabelUser.ToolTipText = resources.GetString("toolStripStatusLabelUser.ToolTipText"); // // SpatialTransformMatrixDialog // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(624, 661); + this.ClientSize = new System.Drawing.Size(604, 661); this.Controls.Add(this.tableLayoutPanelMain); + this.Controls.Add(this.statusStrip); this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); - this.MinimumSize = new System.Drawing.Size(640, 700); + this.MinimumSize = new System.Drawing.Size(620, 700); this.Name = "SpatialTransformMatrixDialog"; this.Text = "TS4231V1 Calibration GUI"; this.tableLayoutPanelMain.ResumeLayout(false); @@ -422,8 +455,10 @@ private void InitializeComponent() this.tableLayoutPanelCoordinates.ResumeLayout(false); this.tableLayoutPanelCoordinates.PerformLayout(); this.flowLayoutPanelBottom.ResumeLayout(false); - this.flowLayoutPanelBottom.PerformLayout(); + this.statusStrip.ResumeLayout(false); + this.statusStrip.PerformLayout(); this.ResumeLayout(false); + this.PerformLayout(); } @@ -453,8 +488,11 @@ private void InitializeComponent() private System.Windows.Forms.Label labelHeaderUser; private System.Windows.Forms.Button buttonCalculate; private System.Windows.Forms.FlowLayoutPanel flowLayoutPanelBottom; - private System.Windows.Forms.Button buttonClose; - private System.Windows.Forms.CheckBox checkBoxApplySpatialTransform; + private System.Windows.Forms.Button buttonCancel; private System.Windows.Forms.Label labelInstructions; + private System.Windows.Forms.Button buttonOK; + private System.Windows.Forms.StatusStrip statusStrip; + private System.Windows.Forms.ToolStripStatusLabel toolStripStatusLabelUser; + private System.Windows.Forms.ToolStripStatusLabel toolStripStatusLabelTS4231; } } diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs index 55929a72..04c83a6b 100644 --- a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs +++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs @@ -7,6 +7,9 @@ namespace OpenEphys.Onix1.Design { + /// + /// Partial class to create a spatial-calibration GUI for . + /// public partial class SpatialTransformMatrixDialog : Form { const byte NumMeasurements = 100; @@ -22,8 +25,6 @@ public partial class SpatialTransformMatrixDialog : Form internal Matrix4x4 NewSpatialTransform { get; private set; } - internal bool ApplySpatialTransform { get; private set; } - internal SpatialTransformMatrixDialog(IObservable positionDataSource, Matrix4x4 currentM) { InitializeComponent(); @@ -36,7 +37,7 @@ internal SpatialTransformMatrixDialog(IObservable pos MeasureButtons = new Button[] { buttonMeasure0, buttonMeasure1, buttonMeasure2, buttonMeasure3 }; } - private void enableButtons(bool enable, byte index) + private void EnableButtons(bool enable, byte index) { for (byte i = 0; i < MeasureButtons.Length; i++) { @@ -45,7 +46,7 @@ private void enableButtons(bool enable, byte index) buttonCalculate.Enabled = enable && InputsValid.All(inputValid => inputValid); } - private void buttonMeasure_Click(object sender, EventArgs e) + private void ButtonMeasure_Click(object sender, EventArgs e) { TextBox[] ts4231TextBoxes = { textBoxTS4231Coordinate0, textBoxTS4231Coordinate1, textBoxTS4231Coordinate2, textBoxTS4231Coordinate3 }; var index = byte.Parse((string)((Button)sender).Tag); @@ -54,7 +55,7 @@ private void buttonMeasure_Click(object sender, EventArgs e) { textBoxStatus.AppendText(string.Format("Measuring coordinate {0}...", index) + Environment.NewLine); MeasureButtons[index].Text = "Cancel"; - enableButtons(false, index); + EnableButtons(false, index); var sharedPositionDataGroups = PositionDataSource .Take(NumMeasurements) @@ -90,16 +91,16 @@ private void buttonMeasure_Click(object sender, EventArgs e) + "Confirm the Lighthouse receivers are within range and unobstructed from Lighthouse transmitters." + Environment.NewLine + Environment.NewLine + "Awaiting user input..." + Environment.NewLine); } - enableButtons(true, index); + EnableButtons(true, index); }); MeasurementCalculationSubscription = sharedPositionDataGroups .Aggregate( (Sum: Vector3.Zero, Count: 0), - (acc, current) => (acc.Sum + Vector3.Transform(current.Position, inverseM), acc.Count + 1), + (acc, current) => (acc.Sum + current.Position, acc.Count + 1), acc => { - TS4231Coordinates[index] = acc.Sum / NumMeasurements; + TS4231Coordinates[index] = Vector3.Transform(acc.Sum / NumMeasurements, inverseM); return (Position: TS4231Coordinates[index], Valid: acc.Count == NumMeasurements); }) .ObserveOn(new ControlScheduler(this)) @@ -113,6 +114,16 @@ private void buttonMeasure_Click(object sender, EventArgs e) finalMeasurement.Position.Y, finalMeasurement.Position.Z); InputsValid[index] = true; + if (InputsValid.Take(4).All(ts4231InputValid => ts4231InputValid)) + { + toolStripStatusLabelTS4231.Image = OpenEphys.Onix1.Design.Properties.Resources.StatusReadyImage; + toolStripStatusLabelTS4231.Text = "All TS4231 coordinates are valid."; + } + else + { + toolStripStatusLabelTS4231.Image = OpenEphys.Onix1.Design.Properties.Resources.StatusBlockedImage; + toolStripStatusLabelTS4231.Text = "At least one TS4231 coordinate is invalid."; + } buttonCalculate.Enabled = InputsValid.All(inputValid => inputValid); } }); @@ -126,19 +137,29 @@ private void buttonMeasure_Click(object sender, EventArgs e) textBoxStatus.AppendText(string.Format("Measurements at coordinate {0} cancelled by user.", index) + Environment.NewLine + Environment.NewLine + "Awaiting user input..." + Environment.NewLine); MeasureButtons[index].Text = "Measure"; - enableButtons(true, index); + EnableButtons(true, index); } } - private void textBoxUserCoordinate_TextChanged(object sender, EventArgs e) + private void TextBoxUserCoordinate_TextChanged(object sender, EventArgs e) { var index = int.Parse((string)((TextBox)sender).Tag); string[] serInputSplit = ((TextBox)sender).Text.Split(','); InputsValid[index] = serInputSplit.Length == 3 && serInputSplit.All(floatCandidate => float.TryParse(floatCandidate, out _)); buttonCalculate.Enabled = InputsValid.All(inputValid => inputValid); + if (InputsValid.Skip(4).Take(4).All(userInputValid => userInputValid)) + { + toolStripStatusLabelUser.Image = OpenEphys.Onix1.Design.Properties.Resources.StatusReadyImage; + toolStripStatusLabelUser.Text = "All user-defined coordinates are valid."; + } + else + { + toolStripStatusLabelUser.Image = OpenEphys.Onix1.Design.Properties.Resources.StatusBlockedImage; + toolStripStatusLabelUser.Text = "At least one user-defined coordinate is invalid."; + } } - private void buttonCalculate_Click(object sender, EventArgs e) + private void ButtonCalculate_Click(object sender, EventArgs e) { var ts4231V1CoordinatesMatrix = new Matrix4x4( TS4231Coordinates[0].X, TS4231Coordinates[0].Y, TS4231Coordinates[0].Z, 1, @@ -165,17 +186,12 @@ private void buttonCalculate_Click(object sender, EventArgs e) textBoxStatus.AppendText(NewSpatialTransform.ToString() + Environment.NewLine + Environment.NewLine); textBoxStatus.AppendText("Awaiting user input..." + Environment.NewLine); - checkBoxApplySpatialTransform.Enabled = true; + buttonOK.Enabled = true; } - private void buttonClose_Click(object sender, EventArgs e) + private void ButtonOKOrCancel_Click(object sender, EventArgs e) { Close(); } - - private void checkBoxApplySpatialTransform_CheckedChanged(object sender, EventArgs e) - { - ApplySpatialTransform = checkBoxApplySpatialTransform.Checked; - } } } diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.resx b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.resx index 3791e7ed..f9416718 100644 --- a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.resx +++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.resx @@ -121,9 +121,17 @@ Follow the instructions below to transform naive TS4231 position data from the base-station reference frame to a user-defined reference frame. For more in-depth instructions, find the corresponding tutorial in Open Ephys' online documentation. 1) For each coordinate: • Place the TS4231V1 device and click the corresponding "Measure" button. - • Input how would like to define the coordinate in the user-defined reference frame. + • Input how you would like to define the coordinate in the user-defined reference frame. 2) Click the "Calculate Spatial Transform" button which enables after all fields are populated with valid data. -3) To automatically set the SpatialTransformMatrix property, check the bottom checkbox and close this GUI. +3) Click "OK" to close this GUI and set the M property as the calculated spatial transform matrix. Click "Cancel" to close this GUI and not set the M property. + + + 36, 14 + + + All four user-defined coordinates must have the following format: "XX, YY, ZZ" or +"XX.XX, YY.YY, ZZ.ZZ" with any number of digits following the decimal before the +spatial transform matrix can be calculated. diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs b/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs index e4bdf73f..4a1e2d7c 100644 --- a/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs +++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs @@ -12,11 +12,12 @@ namespace OpenEphys.Onix1.Design { /// - /// Provides a user interface editor that displays a dialog for selecting - /// members of a workflow expression type. + /// Provides a user interface editor that displays a spatial-calibration dialog + /// for a . /// public class SpatialTransformMatrixEditor : DataSourceTypeEditor { + /// public SpatialTransformMatrixEditor() : base(DataSource.Output, typeof(void)) { @@ -28,11 +29,7 @@ public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext contex return UITypeEditorEditStyle.Modal; } - protected virtual IObservable GetData(IObservable> source) - { - return source.Merge().Select(x => x as TS4231V1PositionDataFrame); - } - + /// public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value) { var editorService = (IWindowsFormsEditorService)provider.GetService(typeof(IWindowsFormsEditorService)); @@ -40,17 +37,15 @@ public override object EditValue(ITypeDescriptorContext context, IServiceProvide if (context != null && editorService != null) { var source = GetDataSource(context, provider); - var dataFrames = GetData(source.Output); - using (var visualizerDialog = new SpatialTransformMatrixDialog(dataFrames, (Matrix4x4)value)) + var dataFrames = source.Output.Merge().Select(x => x as TS4231V1PositionDataFrame); + using var visualizerDialog = new SpatialTransformMatrixDialog(dataFrames, (Matrix4x4)value); + if (!editorState.WorkflowRunning) + { + throw new InvalidOperationException("Workflow must be running to open this GUI."); + } + else if (editorService.ShowDialog(visualizerDialog) == DialogResult.OK) { - if (!editorState.WorkflowRunning) - { - throw new InvalidOperationException("Workflow must be running to open this GUI."); - } - else if (editorService.ShowDialog(visualizerDialog) == DialogResult.OK && visualizerDialog.ApplySpatialTransform) - { - return visualizerDialog.NewSpatialTransform; - } + return visualizerDialog.NewSpatialTransform; } } return base.EditValue(context, provider, value); From 2cf5cd682086f43bf75eee323754289a4c01539b Mon Sep 17 00:00:00 2001 From: cjsha Date: Wed, 11 Jun 2025 15:57:27 -0400 Subject: [PATCH 08/17] Architectural changes - The operator is now an included workflow comprising of ts4231 source node and a spatial transform node - The property that's set by the dialog is a struct containing pre-transform coordinates, post-transform coordinates and spatial transform matrix instead of just the matrix - Add workflows to ItemGroup in Onix1.csproj - Revert TS4231V1PositionData - Dialog changes: - Add X, Y, Z labels - Add a textbox for each component of each coordinate instead of one textbox - Add a textbox & label for displaying Spatial Transform Matrix - Change status messages TextBox to RichTextBox which allows changing font color and using newline characters instead of environment.newline. - Automatically calculate transform matrix when inputs are valid (avoids decoupling sets of pre-transform & post-transform coordinates and the spatial transform) - Simplify bottom toolstrip behavior - Move event handlers to top and helper methods underneath - Change instructions in top label according to the above changes --- .bonsai/Bonsai.config | 5 + .../debug/spatial-transform-test.editor | 37 ++ .../debug/spatial-transform-test.layout | 22 + .../SpatialTransformMatrixDialog.Designer.cs | 618 +++++++++++------- .../SpatialTransformMatrixDialog.cs | 259 +++++--- .../SpatialTransformMatrixDialog.resx | 24 +- .../SpatialTransformMatrixEditor.cs | 11 +- OpenEphys.Onix1/OpenEphys.Onix1.csproj | 4 + OpenEphys.Onix1/TS4231V1PositionData.cs | 17 +- OpenEphys.Onix1/TS4231V1SpatialTransform.cs | 89 +++ .../TS4231V1TransformedPositionData.bonsai | 65 ++ spatial-transform_issue-427.patch | 599 +++++++++++++++++ 12 files changed, 1387 insertions(+), 363 deletions(-) create mode 100644 .bonsai/Settings/artifacts/bin/OpenEphys.Onix1.Design/debug/spatial-transform-test.editor create mode 100644 .bonsai/Settings/artifacts/bin/OpenEphys.Onix1.Design/debug/spatial-transform-test.layout create mode 100644 OpenEphys.Onix1/TS4231V1SpatialTransform.cs create mode 100644 OpenEphys.Onix1/TS4231V1TransformedPositionData.bonsai create mode 100644 spatial-transform_issue-427.patch diff --git a/.bonsai/Bonsai.config b/.bonsai/Bonsai.config index 5a4a1e2a..e0dcade2 100644 --- a/.bonsai/Bonsai.config +++ b/.bonsai/Bonsai.config @@ -48,14 +48,19 @@ + + + + + diff --git a/.bonsai/Settings/artifacts/bin/OpenEphys.Onix1.Design/debug/spatial-transform-test.editor b/.bonsai/Settings/artifacts/bin/OpenEphys.Onix1.Design/debug/spatial-transform-test.editor new file mode 100644 index 00000000..bbd6dca8 --- /dev/null +++ b/.bonsai/Settings/artifacts/bin/OpenEphys.Onix1.Design/debug/spatial-transform-test.editor @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.bonsai/Settings/artifacts/bin/OpenEphys.Onix1.Design/debug/spatial-transform-test.layout b/.bonsai/Settings/artifacts/bin/OpenEphys.Onix1.Design/debug/spatial-transform-test.layout new file mode 100644 index 00000000..ad8bce08 --- /dev/null +++ b/.bonsai/Settings/artifacts/bin/OpenEphys.Onix1.Design/debug/spatial-transform-test.layout @@ -0,0 +1,22 @@ + + + + + 26 + 26 + + + 416 + 279 + + OpenEphys.Onix1.Design.Vector3Visualizer + + + 640 + 0 + 1.1 + true + + + + \ No newline at end of file diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.Designer.cs b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.Designer.cs index 17e2994e..39c04818 100644 --- a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.Designer.cs +++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.Designer.cs @@ -33,13 +33,23 @@ private void InitializeComponent() System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(SpatialTransformMatrixDialog)); this.tableLayoutPanelMain = new System.Windows.Forms.TableLayoutPanel(); this.groupBoxStatus = new System.Windows.Forms.GroupBox(); - this.textBoxStatus = new System.Windows.Forms.TextBox(); + this.richTextBoxStatus = new System.Windows.Forms.RichTextBox(); this.labelInstructions = new System.Windows.Forms.Label(); this.tableLayoutPanelCoordinates = new System.Windows.Forms.TableLayoutPanel(); - this.textBoxUserCoordinate3 = new System.Windows.Forms.TextBox(); - this.textBoxUserCoordinate2 = new System.Windows.Forms.TextBox(); - this.textBoxUserCoordinate1 = new System.Windows.Forms.TextBox(); - this.textBoxUserCoordinate0 = new System.Windows.Forms.TextBox(); + this.labelZ = new System.Windows.Forms.Label(); + this.labelY = new System.Windows.Forms.Label(); + this.textBoxUserCoordinate3Z = new System.Windows.Forms.TextBox(); + this.textBoxUserCoordinate3Y = new System.Windows.Forms.TextBox(); + this.textBoxUserCoordinate3X = new System.Windows.Forms.TextBox(); + this.textBoxUserCoordinate2Z = new System.Windows.Forms.TextBox(); + this.textBoxUserCoordinate2Y = new System.Windows.Forms.TextBox(); + this.textBoxUserCoordinate2X = new System.Windows.Forms.TextBox(); + this.textBoxUserCoordinate1Z = new System.Windows.Forms.TextBox(); + this.textBoxUserCoordinate1Y = new System.Windows.Forms.TextBox(); + this.textBoxUserCoordinate0Z = new System.Windows.Forms.TextBox(); + this.textBoxUserCoordinate0Y = new System.Windows.Forms.TextBox(); + this.textBoxUserCoordinate1X = new System.Windows.Forms.TextBox(); + this.labelXyz = new System.Windows.Forms.Label(); this.textBoxTS4231Coordinate3 = new System.Windows.Forms.TextBox(); this.textBoxTS4231Coordinate2 = new System.Windows.Forms.TextBox(); this.textBoxTS4231Coordinate1 = new System.Windows.Forms.TextBox(); @@ -50,21 +60,25 @@ private void InitializeComponent() this.labelCoordinate2 = new System.Windows.Forms.Label(); this.labelCoordinate1 = new System.Windows.Forms.Label(); this.buttonMeasure0 = new System.Windows.Forms.Button(); - this.labelHeaderTS4231 = new System.Windows.Forms.Label(); + this.labelTS4231 = new System.Windows.Forms.Label(); this.labelCoordinate0 = new System.Windows.Forms.Label(); this.textBoxTS4231Coordinate0 = new System.Windows.Forms.TextBox(); - this.labelHeaderUser = new System.Windows.Forms.Label(); - this.buttonCalculate = new System.Windows.Forms.Button(); + this.textBoxUserCoordinate0X = new System.Windows.Forms.TextBox(); + this.labelUser = new System.Windows.Forms.Label(); + this.labelX = new System.Windows.Forms.Label(); this.flowLayoutPanelBottom = new System.Windows.Forms.FlowLayoutPanel(); this.buttonCancel = new System.Windows.Forms.Button(); this.buttonOK = new System.Windows.Forms.Button(); + this.tableLayoutPanelSpatialMatrix = new System.Windows.Forms.TableLayoutPanel(); + this.labelSpatialMatrix = new System.Windows.Forms.Label(); + this.textBoxSpatialTransformMatrix = new System.Windows.Forms.TextBox(); this.statusStrip = new System.Windows.Forms.StatusStrip(); - this.toolStripStatusLabelTS4231 = new System.Windows.Forms.ToolStripStatusLabel(); - this.toolStripStatusLabelUser = new System.Windows.Forms.ToolStripStatusLabel(); + this.toolStripStatusLabel = new System.Windows.Forms.ToolStripStatusLabel(); this.tableLayoutPanelMain.SuspendLayout(); this.groupBoxStatus.SuspendLayout(); this.tableLayoutPanelCoordinates.SuspendLayout(); this.flowLayoutPanelBottom.SuspendLayout(); + this.tableLayoutPanelSpatialMatrix.SuspendLayout(); this.statusStrip.SuspendLayout(); this.SuspendLayout(); // @@ -72,183 +86,292 @@ private void InitializeComponent() // this.tableLayoutPanelMain.ColumnCount = 1; this.tableLayoutPanelMain.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); - this.tableLayoutPanelMain.Controls.Add(this.groupBoxStatus, 0, 2); + this.tableLayoutPanelMain.Controls.Add(this.groupBoxStatus, 0, 3); this.tableLayoutPanelMain.Controls.Add(this.labelInstructions, 0, 0); this.tableLayoutPanelMain.Controls.Add(this.tableLayoutPanelCoordinates, 0, 1); - this.tableLayoutPanelMain.Controls.Add(this.buttonCalculate, 0, 3); this.tableLayoutPanelMain.Controls.Add(this.flowLayoutPanelBottom, 0, 4); + this.tableLayoutPanelMain.Controls.Add(this.tableLayoutPanelSpatialMatrix, 0, 2); this.tableLayoutPanelMain.Dock = System.Windows.Forms.DockStyle.Fill; this.tableLayoutPanelMain.Location = new System.Drawing.Point(0, 0); this.tableLayoutPanelMain.Name = "tableLayoutPanelMain"; this.tableLayoutPanelMain.RowCount = 5; this.tableLayoutPanelMain.RowStyles.Add(new System.Windows.Forms.RowStyle()); this.tableLayoutPanelMain.RowStyles.Add(new System.Windows.Forms.RowStyle()); - this.tableLayoutPanelMain.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F)); this.tableLayoutPanelMain.RowStyles.Add(new System.Windows.Forms.RowStyle()); + this.tableLayoutPanelMain.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F)); this.tableLayoutPanelMain.RowStyles.Add(new System.Windows.Forms.RowStyle()); - this.tableLayoutPanelMain.Size = new System.Drawing.Size(604, 639); - this.tableLayoutPanelMain.TabIndex = 7; + this.tableLayoutPanelMain.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 20F)); + this.tableLayoutPanelMain.Size = new System.Drawing.Size(624, 540); + this.tableLayoutPanelMain.TabIndex = 0; // // groupBoxStatus // - this.groupBoxStatus.Controls.Add(this.textBoxStatus); + this.groupBoxStatus.Controls.Add(this.richTextBoxStatus); this.groupBoxStatus.Dock = System.Windows.Forms.DockStyle.Fill; - this.groupBoxStatus.Location = new System.Drawing.Point(3, 263); + this.groupBoxStatus.Location = new System.Drawing.Point(3, 372); this.groupBoxStatus.Name = "groupBoxStatus"; - this.groupBoxStatus.Size = new System.Drawing.Size(598, 308); - this.groupBoxStatus.TabIndex = 6; + this.groupBoxStatus.Size = new System.Drawing.Size(618, 129); + this.groupBoxStatus.TabIndex = 1000; this.groupBoxStatus.TabStop = false; this.groupBoxStatus.Text = "Status Messages"; // - // textBoxStatus + // richTextBoxStatus // - this.textBoxStatus.AcceptsReturn = true; - this.textBoxStatus.AcceptsTab = true; - this.textBoxStatus.Dock = System.Windows.Forms.DockStyle.Fill; - this.textBoxStatus.Location = new System.Drawing.Point(3, 16); - this.textBoxStatus.Multiline = true; - this.textBoxStatus.Name = "textBoxStatus"; - this.textBoxStatus.ReadOnly = true; - this.textBoxStatus.ScrollBars = System.Windows.Forms.ScrollBars.Vertical; - this.textBoxStatus.Size = new System.Drawing.Size(592, 289); - this.textBoxStatus.TabIndex = 3; - this.textBoxStatus.Text = "Awaiting user input...\r\n"; + this.richTextBoxStatus.Dock = System.Windows.Forms.DockStyle.Fill; + this.richTextBoxStatus.Location = new System.Drawing.Point(3, 16); + this.richTextBoxStatus.Name = "richTextBoxStatus"; + this.richTextBoxStatus.ReadOnly = true; + this.richTextBoxStatus.ScrollBars = System.Windows.Forms.RichTextBoxScrollBars.ForcedVertical; + this.richTextBoxStatus.Size = new System.Drawing.Size(612, 110); + this.richTextBoxStatus.TabIndex = 1000; + this.richTextBoxStatus.TabStop = false; + this.richTextBoxStatus.Text = ""; // // labelInstructions // this.labelInstructions.AutoSize = true; this.labelInstructions.Location = new System.Drawing.Point(3, 0); this.labelInstructions.Name = "labelInstructions"; - this.labelInstructions.Size = new System.Drawing.Size(596, 104); - this.labelInstructions.TabIndex = 4; + this.labelInstructions.Size = new System.Drawing.Size(596, 117); + this.labelInstructions.TabIndex = 1000; this.labelInstructions.Text = resources.GetString("labelInstructions.Text"); // // tableLayoutPanelCoordinates // - this.tableLayoutPanelCoordinates.AutoSize = true; - this.tableLayoutPanelCoordinates.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; - this.tableLayoutPanelCoordinates.ColumnCount = 4; + this.tableLayoutPanelCoordinates.ColumnCount = 6; this.tableLayoutPanelCoordinates.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); this.tableLayoutPanelCoordinates.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); - this.tableLayoutPanelCoordinates.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F)); - this.tableLayoutPanelCoordinates.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F)); - this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxUserCoordinate3, 3, 4); - this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxUserCoordinate2, 3, 3); - this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxUserCoordinate1, 3, 2); - this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxUserCoordinate0, 3, 1); - this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxTS4231Coordinate3, 2, 4); - this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxTS4231Coordinate2, 2, 3); - this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxTS4231Coordinate1, 2, 2); - this.tableLayoutPanelCoordinates.Controls.Add(this.buttonMeasure3, 1, 4); - this.tableLayoutPanelCoordinates.Controls.Add(this.buttonMeasure2, 1, 3); - this.tableLayoutPanelCoordinates.Controls.Add(this.buttonMeasure1, 1, 2); - this.tableLayoutPanelCoordinates.Controls.Add(this.labelCoordinate3, 0, 4); - this.tableLayoutPanelCoordinates.Controls.Add(this.labelCoordinate2, 0, 3); - this.tableLayoutPanelCoordinates.Controls.Add(this.labelCoordinate1, 0, 2); - this.tableLayoutPanelCoordinates.Controls.Add(this.buttonMeasure0, 1, 1); - this.tableLayoutPanelCoordinates.Controls.Add(this.labelHeaderTS4231, 1, 0); - this.tableLayoutPanelCoordinates.Controls.Add(this.labelCoordinate0, 0, 1); - this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxTS4231Coordinate0, 2, 1); - this.tableLayoutPanelCoordinates.Controls.Add(this.labelHeaderUser, 3, 0); + this.tableLayoutPanelCoordinates.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 180F)); + this.tableLayoutPanelCoordinates.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 33.33333F)); + this.tableLayoutPanelCoordinates.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 33.33334F)); + this.tableLayoutPanelCoordinates.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 33.33334F)); + this.tableLayoutPanelCoordinates.Controls.Add(this.labelZ, 6, 1); + this.tableLayoutPanelCoordinates.Controls.Add(this.labelY, 4, 1); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxUserCoordinate3Z, 5, 5); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxUserCoordinate3Y, 4, 5); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxUserCoordinate3X, 3, 5); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxUserCoordinate2Z, 5, 4); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxUserCoordinate2Y, 4, 4); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxUserCoordinate2X, 3, 4); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxUserCoordinate1Z, 5, 3); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxUserCoordinate1Y, 4, 3); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxUserCoordinate0Z, 5, 2); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxUserCoordinate0Y, 4, 2); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxUserCoordinate1X, 3, 3); + this.tableLayoutPanelCoordinates.Controls.Add(this.labelXyz, 2, 1); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxTS4231Coordinate3, 2, 5); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxTS4231Coordinate2, 2, 4); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxTS4231Coordinate1, 2, 3); + this.tableLayoutPanelCoordinates.Controls.Add(this.buttonMeasure3, 1, 5); + this.tableLayoutPanelCoordinates.Controls.Add(this.buttonMeasure2, 1, 4); + this.tableLayoutPanelCoordinates.Controls.Add(this.buttonMeasure1, 1, 3); + this.tableLayoutPanelCoordinates.Controls.Add(this.labelCoordinate3, 0, 5); + this.tableLayoutPanelCoordinates.Controls.Add(this.labelCoordinate2, 0, 4); + this.tableLayoutPanelCoordinates.Controls.Add(this.labelCoordinate1, 0, 3); + this.tableLayoutPanelCoordinates.Controls.Add(this.buttonMeasure0, 1, 2); + this.tableLayoutPanelCoordinates.Controls.Add(this.labelTS4231, 1, 0); + this.tableLayoutPanelCoordinates.Controls.Add(this.labelCoordinate0, 0, 2); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxTS4231Coordinate0, 2, 2); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxUserCoordinate0X, 3, 2); + this.tableLayoutPanelCoordinates.Controls.Add(this.labelUser, 3, 0); + this.tableLayoutPanelCoordinates.Controls.Add(this.labelX, 3, 1); this.tableLayoutPanelCoordinates.Dock = System.Windows.Forms.DockStyle.Fill; - this.tableLayoutPanelCoordinates.Location = new System.Drawing.Point(3, 107); + this.tableLayoutPanelCoordinates.Location = new System.Drawing.Point(3, 120); this.tableLayoutPanelCoordinates.Name = "tableLayoutPanelCoordinates"; - this.tableLayoutPanelCoordinates.RowCount = 5; + this.tableLayoutPanelCoordinates.RowCount = 6; + this.tableLayoutPanelCoordinates.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 30F)); this.tableLayoutPanelCoordinates.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 30F)); this.tableLayoutPanelCoordinates.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 30F)); this.tableLayoutPanelCoordinates.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 30F)); this.tableLayoutPanelCoordinates.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 30F)); this.tableLayoutPanelCoordinates.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 30F)); - this.tableLayoutPanelCoordinates.Size = new System.Drawing.Size(598, 150); - this.tableLayoutPanelCoordinates.TabIndex = 5; - this.tableLayoutPanelCoordinates.Tag = "6"; - // - // textBoxUserCoordinate3 - // - this.textBoxUserCoordinate3.Dock = System.Windows.Forms.DockStyle.Fill; - this.textBoxUserCoordinate3.Location = new System.Drawing.Point(385, 123); - this.textBoxUserCoordinate3.MinimumSize = new System.Drawing.Size(150, 4); - this.textBoxUserCoordinate3.Name = "textBoxUserCoordinate3"; - this.textBoxUserCoordinate3.Size = new System.Drawing.Size(210, 20); - this.textBoxUserCoordinate3.TabIndex = 39; - this.textBoxUserCoordinate3.Tag = "7"; - this.textBoxUserCoordinate3.TextChanged += new System.EventHandler(this.TextBoxUserCoordinate_TextChanged); - // - // textBoxUserCoordinate2 - // - this.textBoxUserCoordinate2.Dock = System.Windows.Forms.DockStyle.Fill; - this.textBoxUserCoordinate2.Location = new System.Drawing.Point(385, 93); - this.textBoxUserCoordinate2.MinimumSize = new System.Drawing.Size(150, 4); - this.textBoxUserCoordinate2.Name = "textBoxUserCoordinate2"; - this.textBoxUserCoordinate2.Size = new System.Drawing.Size(210, 20); - this.textBoxUserCoordinate2.TabIndex = 38; - this.textBoxUserCoordinate2.Tag = "6"; - this.textBoxUserCoordinate2.TextChanged += new System.EventHandler(this.TextBoxUserCoordinate_TextChanged); - // - // textBoxUserCoordinate1 - // - this.textBoxUserCoordinate1.Dock = System.Windows.Forms.DockStyle.Fill; - this.textBoxUserCoordinate1.Location = new System.Drawing.Point(385, 63); - this.textBoxUserCoordinate1.MinimumSize = new System.Drawing.Size(150, 4); - this.textBoxUserCoordinate1.Name = "textBoxUserCoordinate1"; - this.textBoxUserCoordinate1.Size = new System.Drawing.Size(210, 20); - this.textBoxUserCoordinate1.TabIndex = 37; - this.textBoxUserCoordinate1.Tag = "5"; - this.textBoxUserCoordinate1.TextChanged += new System.EventHandler(this.TextBoxUserCoordinate_TextChanged); - // - // textBoxUserCoordinate0 - // - this.textBoxUserCoordinate0.Dock = System.Windows.Forms.DockStyle.Fill; - this.textBoxUserCoordinate0.Location = new System.Drawing.Point(385, 33); - this.textBoxUserCoordinate0.MinimumSize = new System.Drawing.Size(150, 4); - this.textBoxUserCoordinate0.Name = "textBoxUserCoordinate0"; - this.textBoxUserCoordinate0.Size = new System.Drawing.Size(210, 20); - this.textBoxUserCoordinate0.TabIndex = 36; - this.textBoxUserCoordinate0.Tag = "4"; - this.textBoxUserCoordinate0.TextChanged += new System.EventHandler(this.TextBoxUserCoordinate_TextChanged); + this.tableLayoutPanelCoordinates.Size = new System.Drawing.Size(618, 180); + this.tableLayoutPanelCoordinates.TabIndex = 0; + this.tableLayoutPanelCoordinates.Tag = "0"; + // + // labelZ + // + this.labelZ.Dock = System.Windows.Forms.DockStyle.Fill; + this.labelZ.Location = new System.Drawing.Point(526, 30); + this.labelZ.Margin = new System.Windows.Forms.Padding(0); + this.labelZ.Name = "labelZ"; + this.labelZ.Size = new System.Drawing.Size(92, 30); + this.labelZ.TabIndex = 1000; + this.labelZ.Text = "Z"; + this.labelZ.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // labelY + // + this.labelY.Dock = System.Windows.Forms.DockStyle.Fill; + this.labelY.Location = new System.Drawing.Point(436, 30); + this.labelY.Margin = new System.Windows.Forms.Padding(0); + this.labelY.Name = "labelY"; + this.labelY.Size = new System.Drawing.Size(90, 30); + this.labelY.TabIndex = 1000; + this.labelY.Text = "Y"; + this.labelY.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // textBoxUserCoordinate3Z + // + this.textBoxUserCoordinate3Z.Dock = System.Windows.Forms.DockStyle.Bottom; + this.textBoxUserCoordinate3Z.Location = new System.Drawing.Point(529, 157); + this.textBoxUserCoordinate3Z.Name = "textBoxUserCoordinate3Z"; + this.textBoxUserCoordinate3Z.Size = new System.Drawing.Size(86, 20); + this.textBoxUserCoordinate3Z.TabIndex = 15; + this.textBoxUserCoordinate3Z.Tag = "11"; + this.textBoxUserCoordinate3Z.TextChanged += new System.EventHandler(this.TextBoxUserCoordinate_TextChanged); + // + // textBoxUserCoordinate3Y + // + this.textBoxUserCoordinate3Y.Dock = System.Windows.Forms.DockStyle.Bottom; + this.textBoxUserCoordinate3Y.Location = new System.Drawing.Point(439, 157); + this.textBoxUserCoordinate3Y.Name = "textBoxUserCoordinate3Y"; + this.textBoxUserCoordinate3Y.Size = new System.Drawing.Size(84, 20); + this.textBoxUserCoordinate3Y.TabIndex = 14; + this.textBoxUserCoordinate3Y.Tag = "10"; + this.textBoxUserCoordinate3Y.TextChanged += new System.EventHandler(this.TextBoxUserCoordinate_TextChanged); + // + // textBoxUserCoordinate3X + // + this.textBoxUserCoordinate3X.Dock = System.Windows.Forms.DockStyle.Bottom; + this.textBoxUserCoordinate3X.Location = new System.Drawing.Point(349, 157); + this.textBoxUserCoordinate3X.Name = "textBoxUserCoordinate3X"; + this.textBoxUserCoordinate3X.Size = new System.Drawing.Size(84, 20); + this.textBoxUserCoordinate3X.TabIndex = 13; + this.textBoxUserCoordinate3X.Tag = "9"; + this.textBoxUserCoordinate3X.TextChanged += new System.EventHandler(this.TextBoxUserCoordinate_TextChanged); + // + // textBoxUserCoordinate2Z + // + this.textBoxUserCoordinate2Z.Dock = System.Windows.Forms.DockStyle.Bottom; + this.textBoxUserCoordinate2Z.Location = new System.Drawing.Point(529, 127); + this.textBoxUserCoordinate2Z.Name = "textBoxUserCoordinate2Z"; + this.textBoxUserCoordinate2Z.Size = new System.Drawing.Size(86, 20); + this.textBoxUserCoordinate2Z.TabIndex = 11; + this.textBoxUserCoordinate2Z.Tag = "8"; + this.textBoxUserCoordinate2Z.TextChanged += new System.EventHandler(this.TextBoxUserCoordinate_TextChanged); + // + // textBoxUserCoordinate2Y + // + this.textBoxUserCoordinate2Y.Dock = System.Windows.Forms.DockStyle.Bottom; + this.textBoxUserCoordinate2Y.Location = new System.Drawing.Point(439, 127); + this.textBoxUserCoordinate2Y.Name = "textBoxUserCoordinate2Y"; + this.textBoxUserCoordinate2Y.Size = new System.Drawing.Size(84, 20); + this.textBoxUserCoordinate2Y.TabIndex = 10; + this.textBoxUserCoordinate2Y.Tag = "7"; + this.textBoxUserCoordinate2Y.TextChanged += new System.EventHandler(this.TextBoxUserCoordinate_TextChanged); + // + // textBoxUserCoordinate2X + // + this.textBoxUserCoordinate2X.Dock = System.Windows.Forms.DockStyle.Bottom; + this.textBoxUserCoordinate2X.Location = new System.Drawing.Point(349, 127); + this.textBoxUserCoordinate2X.Name = "textBoxUserCoordinate2X"; + this.textBoxUserCoordinate2X.Size = new System.Drawing.Size(84, 20); + this.textBoxUserCoordinate2X.TabIndex = 9; + this.textBoxUserCoordinate2X.Tag = "6"; + this.textBoxUserCoordinate2X.TextChanged += new System.EventHandler(this.TextBoxUserCoordinate_TextChanged); + // + // textBoxUserCoordinate1Z + // + this.textBoxUserCoordinate1Z.Dock = System.Windows.Forms.DockStyle.Bottom; + this.textBoxUserCoordinate1Z.Location = new System.Drawing.Point(529, 97); + this.textBoxUserCoordinate1Z.Name = "textBoxUserCoordinate1Z"; + this.textBoxUserCoordinate1Z.Size = new System.Drawing.Size(86, 20); + this.textBoxUserCoordinate1Z.TabIndex = 7; + this.textBoxUserCoordinate1Z.Tag = "5"; + this.textBoxUserCoordinate1Z.TextChanged += new System.EventHandler(this.TextBoxUserCoordinate_TextChanged); + // + // textBoxUserCoordinate1Y + // + this.textBoxUserCoordinate1Y.Dock = System.Windows.Forms.DockStyle.Bottom; + this.textBoxUserCoordinate1Y.Location = new System.Drawing.Point(439, 97); + this.textBoxUserCoordinate1Y.Name = "textBoxUserCoordinate1Y"; + this.textBoxUserCoordinate1Y.Size = new System.Drawing.Size(84, 20); + this.textBoxUserCoordinate1Y.TabIndex = 6; + this.textBoxUserCoordinate1Y.Tag = "4"; + this.textBoxUserCoordinate1Y.TextChanged += new System.EventHandler(this.TextBoxUserCoordinate_TextChanged); + // + // textBoxUserCoordinate0Z + // + this.textBoxUserCoordinate0Z.Dock = System.Windows.Forms.DockStyle.Bottom; + this.textBoxUserCoordinate0Z.Location = new System.Drawing.Point(529, 67); + this.textBoxUserCoordinate0Z.Name = "textBoxUserCoordinate0Z"; + this.textBoxUserCoordinate0Z.Size = new System.Drawing.Size(86, 20); + this.textBoxUserCoordinate0Z.TabIndex = 3; + this.textBoxUserCoordinate0Z.Tag = "2"; + this.textBoxUserCoordinate0Z.TextChanged += new System.EventHandler(this.TextBoxUserCoordinate_TextChanged); + // + // textBoxUserCoordinate0Y + // + this.textBoxUserCoordinate0Y.Dock = System.Windows.Forms.DockStyle.Bottom; + this.textBoxUserCoordinate0Y.Location = new System.Drawing.Point(439, 67); + this.textBoxUserCoordinate0Y.Name = "textBoxUserCoordinate0Y"; + this.textBoxUserCoordinate0Y.Size = new System.Drawing.Size(84, 20); + this.textBoxUserCoordinate0Y.TabIndex = 2; + this.textBoxUserCoordinate0Y.Tag = "1"; + this.textBoxUserCoordinate0Y.TextChanged += new System.EventHandler(this.TextBoxUserCoordinate_TextChanged); + // + // textBoxUserCoordinate1X + // + this.textBoxUserCoordinate1X.Dock = System.Windows.Forms.DockStyle.Bottom; + this.textBoxUserCoordinate1X.Location = new System.Drawing.Point(349, 97); + this.textBoxUserCoordinate1X.Name = "textBoxUserCoordinate1X"; + this.textBoxUserCoordinate1X.Size = new System.Drawing.Size(84, 20); + this.textBoxUserCoordinate1X.TabIndex = 5; + this.textBoxUserCoordinate1X.Tag = "3"; + this.textBoxUserCoordinate1X.TextChanged += new System.EventHandler(this.TextBoxUserCoordinate_TextChanged); + // + // labelXyz + // + this.labelXyz.Dock = System.Windows.Forms.DockStyle.Fill; + this.labelXyz.Location = new System.Drawing.Point(166, 30); + this.labelXyz.Margin = new System.Windows.Forms.Padding(0); + this.labelXyz.Name = "labelXyz"; + this.labelXyz.Size = new System.Drawing.Size(180, 30); + this.labelXyz.TabIndex = 1000; + this.labelXyz.Text = "X, Y, Z"; + this.labelXyz.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; // // textBoxTS4231Coordinate3 // - this.textBoxTS4231Coordinate3.Dock = System.Windows.Forms.DockStyle.Fill; - this.textBoxTS4231Coordinate3.Location = new System.Drawing.Point(169, 123); - this.textBoxTS4231Coordinate3.MinimumSize = new System.Drawing.Size(150, 4); + this.textBoxTS4231Coordinate3.Dock = System.Windows.Forms.DockStyle.Bottom; + this.textBoxTS4231Coordinate3.Location = new System.Drawing.Point(169, 157); this.textBoxTS4231Coordinate3.Name = "textBoxTS4231Coordinate3"; this.textBoxTS4231Coordinate3.ReadOnly = true; - this.textBoxTS4231Coordinate3.Size = new System.Drawing.Size(210, 20); - this.textBoxTS4231Coordinate3.TabIndex = 33; + this.textBoxTS4231Coordinate3.Size = new System.Drawing.Size(174, 20); + this.textBoxTS4231Coordinate3.TabIndex = 1000; this.textBoxTS4231Coordinate3.TabStop = false; this.textBoxTS4231Coordinate3.Tag = "3"; // // textBoxTS4231Coordinate2 // - this.textBoxTS4231Coordinate2.Dock = System.Windows.Forms.DockStyle.Fill; - this.textBoxTS4231Coordinate2.Location = new System.Drawing.Point(169, 93); - this.textBoxTS4231Coordinate2.MinimumSize = new System.Drawing.Size(150, 4); + this.textBoxTS4231Coordinate2.Dock = System.Windows.Forms.DockStyle.Bottom; + this.textBoxTS4231Coordinate2.Location = new System.Drawing.Point(169, 127); this.textBoxTS4231Coordinate2.Name = "textBoxTS4231Coordinate2"; this.textBoxTS4231Coordinate2.ReadOnly = true; - this.textBoxTS4231Coordinate2.Size = new System.Drawing.Size(210, 20); - this.textBoxTS4231Coordinate2.TabIndex = 32; + this.textBoxTS4231Coordinate2.Size = new System.Drawing.Size(174, 20); + this.textBoxTS4231Coordinate2.TabIndex = 1000; this.textBoxTS4231Coordinate2.TabStop = false; this.textBoxTS4231Coordinate2.Tag = "2"; // // textBoxTS4231Coordinate1 // - this.textBoxTS4231Coordinate1.Dock = System.Windows.Forms.DockStyle.Fill; - this.textBoxTS4231Coordinate1.Location = new System.Drawing.Point(169, 63); - this.textBoxTS4231Coordinate1.MinimumSize = new System.Drawing.Size(150, 4); + this.textBoxTS4231Coordinate1.Dock = System.Windows.Forms.DockStyle.Bottom; + this.textBoxTS4231Coordinate1.Location = new System.Drawing.Point(169, 97); this.textBoxTS4231Coordinate1.Name = "textBoxTS4231Coordinate1"; this.textBoxTS4231Coordinate1.ReadOnly = true; - this.textBoxTS4231Coordinate1.Size = new System.Drawing.Size(210, 20); - this.textBoxTS4231Coordinate1.TabIndex = 31; + this.textBoxTS4231Coordinate1.Size = new System.Drawing.Size(174, 20); + this.textBoxTS4231Coordinate1.TabIndex = 1000; this.textBoxTS4231Coordinate1.TabStop = false; this.textBoxTS4231Coordinate1.Tag = "1"; // // buttonMeasure3 // - this.buttonMeasure3.Location = new System.Drawing.Point(83, 123); + this.buttonMeasure3.Anchor = System.Windows.Forms.AnchorStyles.Left; + this.buttonMeasure3.Location = new System.Drawing.Point(83, 153); this.buttonMeasure3.Name = "buttonMeasure3"; this.buttonMeasure3.Size = new System.Drawing.Size(80, 24); - this.buttonMeasure3.TabIndex = 29; + this.buttonMeasure3.TabIndex = 12; this.buttonMeasure3.Tag = "3"; this.buttonMeasure3.Text = "Measure"; this.buttonMeasure3.UseVisualStyleBackColor = true; @@ -256,10 +379,11 @@ private void InitializeComponent() // // buttonMeasure2 // - this.buttonMeasure2.Location = new System.Drawing.Point(83, 93); + this.buttonMeasure2.Anchor = System.Windows.Forms.AnchorStyles.Left; + this.buttonMeasure2.Location = new System.Drawing.Point(83, 123); this.buttonMeasure2.Name = "buttonMeasure2"; this.buttonMeasure2.Size = new System.Drawing.Size(80, 24); - this.buttonMeasure2.TabIndex = 26; + this.buttonMeasure2.TabIndex = 8; this.buttonMeasure2.Tag = "2"; this.buttonMeasure2.Text = "Measure"; this.buttonMeasure2.UseVisualStyleBackColor = true; @@ -268,10 +392,10 @@ private void InitializeComponent() // buttonMeasure1 // this.buttonMeasure1.Anchor = System.Windows.Forms.AnchorStyles.Left; - this.buttonMeasure1.Location = new System.Drawing.Point(83, 63); + this.buttonMeasure1.Location = new System.Drawing.Point(83, 93); this.buttonMeasure1.Name = "buttonMeasure1"; this.buttonMeasure1.Size = new System.Drawing.Size(80, 24); - this.buttonMeasure1.TabIndex = 23; + this.buttonMeasure1.TabIndex = 4; this.buttonMeasure1.Tag = "1"; this.buttonMeasure1.Text = "Measure"; this.buttonMeasure1.UseVisualStyleBackColor = true; @@ -279,99 +403,106 @@ private void InitializeComponent() // // labelCoordinate3 // - this.labelCoordinate3.Dock = System.Windows.Forms.DockStyle.Fill; - this.labelCoordinate3.Location = new System.Drawing.Point(3, 120); + this.labelCoordinate3.Location = new System.Drawing.Point(3, 150); this.labelCoordinate3.Name = "labelCoordinate3"; this.labelCoordinate3.Size = new System.Drawing.Size(74, 30); - this.labelCoordinate3.TabIndex = 18; + this.labelCoordinate3.TabIndex = 1000; this.labelCoordinate3.Text = "Coordinate 3:"; this.labelCoordinate3.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; // // labelCoordinate2 // - this.labelCoordinate2.Dock = System.Windows.Forms.DockStyle.Fill; - this.labelCoordinate2.Location = new System.Drawing.Point(3, 90); + this.labelCoordinate2.Location = new System.Drawing.Point(3, 120); this.labelCoordinate2.Name = "labelCoordinate2"; this.labelCoordinate2.Size = new System.Drawing.Size(74, 30); - this.labelCoordinate2.TabIndex = 16; + this.labelCoordinate2.TabIndex = 100; this.labelCoordinate2.Text = "Coordinate 2:"; this.labelCoordinate2.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; // // labelCoordinate1 // - this.labelCoordinate1.Location = new System.Drawing.Point(3, 60); + this.labelCoordinate1.Location = new System.Drawing.Point(3, 90); this.labelCoordinate1.Name = "labelCoordinate1"; this.labelCoordinate1.Size = new System.Drawing.Size(74, 30); - this.labelCoordinate1.TabIndex = 10; + this.labelCoordinate1.TabIndex = 1000; this.labelCoordinate1.Text = "Coordinate 1:"; this.labelCoordinate1.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; // // buttonMeasure0 // - this.buttonMeasure0.Location = new System.Drawing.Point(83, 33); + this.buttonMeasure0.Location = new System.Drawing.Point(83, 63); this.buttonMeasure0.Name = "buttonMeasure0"; this.buttonMeasure0.Size = new System.Drawing.Size(80, 24); - this.buttonMeasure0.TabIndex = 1; + this.buttonMeasure0.TabIndex = 0; this.buttonMeasure0.Tag = "0"; this.buttonMeasure0.Text = "Measure"; this.buttonMeasure0.UseVisualStyleBackColor = true; this.buttonMeasure0.Click += new System.EventHandler(this.ButtonMeasure_Click); // - // labelHeaderTS4231 + // labelTS4231 // - this.tableLayoutPanelCoordinates.SetColumnSpan(this.labelHeaderTS4231, 2); - this.labelHeaderTS4231.Dock = System.Windows.Forms.DockStyle.Fill; - this.labelHeaderTS4231.Location = new System.Drawing.Point(80, 0); - this.labelHeaderTS4231.Margin = new System.Windows.Forms.Padding(0); - this.labelHeaderTS4231.Name = "labelHeaderTS4231"; - this.labelHeaderTS4231.Size = new System.Drawing.Size(302, 30); - this.labelHeaderTS4231.TabIndex = 0; - this.labelHeaderTS4231.Text = "Naive TS4231 Coordinates"; - this.labelHeaderTS4231.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + this.labelTS4231.AutoSize = true; + this.tableLayoutPanelCoordinates.SetColumnSpan(this.labelTS4231, 2); + this.labelTS4231.Dock = System.Windows.Forms.DockStyle.Fill; + this.labelTS4231.Location = new System.Drawing.Point(80, 0); + this.labelTS4231.Margin = new System.Windows.Forms.Padding(0); + this.labelTS4231.Name = "labelTS4231"; + this.labelTS4231.Size = new System.Drawing.Size(266, 30); + this.labelTS4231.TabIndex = 1000; + this.labelTS4231.Text = "TS4231 Coordinates"; + this.labelTS4231.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; // // labelCoordinate0 // - this.labelCoordinate0.Location = new System.Drawing.Point(3, 30); + this.labelCoordinate0.Location = new System.Drawing.Point(3, 60); this.labelCoordinate0.Name = "labelCoordinate0"; this.labelCoordinate0.Size = new System.Drawing.Size(74, 30); - this.labelCoordinate0.TabIndex = 2; + this.labelCoordinate0.TabIndex = 1000; this.labelCoordinate0.Text = "Coordinate 0:"; this.labelCoordinate0.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; // // textBoxTS4231Coordinate0 // - this.textBoxTS4231Coordinate0.Dock = System.Windows.Forms.DockStyle.Fill; - this.textBoxTS4231Coordinate0.Location = new System.Drawing.Point(169, 33); - this.textBoxTS4231Coordinate0.MinimumSize = new System.Drawing.Size(150, 4); + this.textBoxTS4231Coordinate0.Dock = System.Windows.Forms.DockStyle.Bottom; + this.textBoxTS4231Coordinate0.Location = new System.Drawing.Point(169, 67); this.textBoxTS4231Coordinate0.Name = "textBoxTS4231Coordinate0"; this.textBoxTS4231Coordinate0.ReadOnly = true; - this.textBoxTS4231Coordinate0.Size = new System.Drawing.Size(210, 20); - this.textBoxTS4231Coordinate0.TabIndex = 30; + this.textBoxTS4231Coordinate0.Size = new System.Drawing.Size(174, 20); + this.textBoxTS4231Coordinate0.TabIndex = 1000; this.textBoxTS4231Coordinate0.TabStop = false; this.textBoxTS4231Coordinate0.Tag = "0"; // - // labelHeaderUser - // - this.labelHeaderUser.Dock = System.Windows.Forms.DockStyle.Fill; - this.labelHeaderUser.Location = new System.Drawing.Point(385, 0); - this.labelHeaderUser.MinimumSize = new System.Drawing.Size(150, 0); - this.labelHeaderUser.Name = "labelHeaderUser"; - this.labelHeaderUser.Size = new System.Drawing.Size(210, 30); - this.labelHeaderUser.TabIndex = 34; - this.labelHeaderUser.Text = "User-Defined Coordinates"; - this.labelHeaderUser.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; - // - // buttonCalculate - // - this.buttonCalculate.Dock = System.Windows.Forms.DockStyle.Fill; - this.buttonCalculate.Enabled = false; - this.buttonCalculate.Location = new System.Drawing.Point(3, 577); - this.buttonCalculate.Name = "buttonCalculate"; - this.buttonCalculate.Size = new System.Drawing.Size(598, 23); - this.buttonCalculate.TabIndex = 7; - this.buttonCalculate.Text = "Calculate Spatial Transform"; - this.buttonCalculate.UseVisualStyleBackColor = true; - this.buttonCalculate.Click += new System.EventHandler(this.ButtonCalculate_Click); + // textBoxUserCoordinate0X + // + this.textBoxUserCoordinate0X.Dock = System.Windows.Forms.DockStyle.Bottom; + this.textBoxUserCoordinate0X.Location = new System.Drawing.Point(349, 67); + this.textBoxUserCoordinate0X.Name = "textBoxUserCoordinate0X"; + this.textBoxUserCoordinate0X.Size = new System.Drawing.Size(84, 20); + this.textBoxUserCoordinate0X.TabIndex = 1; + this.textBoxUserCoordinate0X.Tag = "0"; + this.textBoxUserCoordinate0X.TextChanged += new System.EventHandler(this.TextBoxUserCoordinate_TextChanged); + // + // labelUser + // + this.tableLayoutPanelCoordinates.SetColumnSpan(this.labelUser, 3); + this.labelUser.Dock = System.Windows.Forms.DockStyle.Fill; + this.labelUser.Location = new System.Drawing.Point(346, 0); + this.labelUser.Margin = new System.Windows.Forms.Padding(0); + this.labelUser.Name = "labelUser"; + this.labelUser.Size = new System.Drawing.Size(272, 30); + this.labelUser.TabIndex = 1000; + this.labelUser.Text = "User Coordinates"; + this.labelUser.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // labelX + // + this.labelX.Dock = System.Windows.Forms.DockStyle.Fill; + this.labelX.Location = new System.Drawing.Point(349, 30); + this.labelX.Name = "labelX"; + this.labelX.Size = new System.Drawing.Size(84, 30); + this.labelX.TabIndex = 1000; + this.labelX.Text = "X"; + this.labelX.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; // // flowLayoutPanelBottom // @@ -380,81 +511,109 @@ private void InitializeComponent() this.flowLayoutPanelBottom.Controls.Add(this.buttonOK); this.flowLayoutPanelBottom.Dock = System.Windows.Forms.DockStyle.Fill; this.flowLayoutPanelBottom.FlowDirection = System.Windows.Forms.FlowDirection.RightToLeft; - this.flowLayoutPanelBottom.Location = new System.Drawing.Point(3, 606); + this.flowLayoutPanelBottom.Location = new System.Drawing.Point(3, 507); this.flowLayoutPanelBottom.Name = "flowLayoutPanelBottom"; - this.flowLayoutPanelBottom.Size = new System.Drawing.Size(598, 30); - this.flowLayoutPanelBottom.TabIndex = 8; + this.flowLayoutPanelBottom.Size = new System.Drawing.Size(618, 30); + this.flowLayoutPanelBottom.TabIndex = 2; // // buttonCancel // this.buttonCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel; - this.buttonCancel.Location = new System.Drawing.Point(515, 3); + this.buttonCancel.Location = new System.Drawing.Point(535, 3); this.buttonCancel.Name = "buttonCancel"; this.buttonCancel.Size = new System.Drawing.Size(80, 24); - this.buttonCancel.TabIndex = 0; + this.buttonCancel.TabIndex = 1; + this.buttonCancel.Tag = "5"; this.buttonCancel.Text = "Cancel"; this.buttonCancel.UseVisualStyleBackColor = true; - this.buttonCancel.Click += new System.EventHandler(this.ButtonOKOrCancel_Click); // // buttonOK // - this.buttonOK.DialogResult = System.Windows.Forms.DialogResult.OK; - this.buttonOK.Enabled = false; - this.buttonOK.Location = new System.Drawing.Point(429, 3); + this.buttonOK.Location = new System.Drawing.Point(449, 3); this.buttonOK.Name = "buttonOK"; this.buttonOK.Size = new System.Drawing.Size(80, 24); - this.buttonOK.TabIndex = 2; + this.buttonOK.TabIndex = 0; + this.buttonOK.Tag = "4"; this.buttonOK.Text = "OK"; this.buttonOK.UseVisualStyleBackColor = true; - this.buttonOK.Click += new System.EventHandler(this.ButtonOKOrCancel_Click); + this.buttonOK.Click += new System.EventHandler(this.ButtonOK_Click); + // + // tableLayoutPanelSpatialMatrix + // + this.tableLayoutPanelSpatialMatrix.AutoSize = true; + this.tableLayoutPanelSpatialMatrix.ColumnCount = 2; + this.tableLayoutPanelSpatialMatrix.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); + this.tableLayoutPanelSpatialMatrix.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); + this.tableLayoutPanelSpatialMatrix.Controls.Add(this.labelSpatialMatrix, 0, 0); + this.tableLayoutPanelSpatialMatrix.Controls.Add(this.textBoxSpatialTransformMatrix, 1, 0); + this.tableLayoutPanelSpatialMatrix.Dock = System.Windows.Forms.DockStyle.Fill; + this.tableLayoutPanelSpatialMatrix.Location = new System.Drawing.Point(3, 306); + this.tableLayoutPanelSpatialMatrix.Name = "tableLayoutPanelSpatialMatrix"; + this.tableLayoutPanelSpatialMatrix.RowCount = 1; + this.tableLayoutPanelSpatialMatrix.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 60F)); + this.tableLayoutPanelSpatialMatrix.Size = new System.Drawing.Size(618, 60); + this.tableLayoutPanelSpatialMatrix.TabIndex = 0; + // + // labelSpatialMatrix + // + this.labelSpatialMatrix.AutoSize = true; + this.labelSpatialMatrix.Dock = System.Windows.Forms.DockStyle.Fill; + this.labelSpatialMatrix.Location = new System.Drawing.Point(3, 0); + this.labelSpatialMatrix.Name = "labelSpatialMatrix"; + this.labelSpatialMatrix.Size = new System.Drawing.Size(123, 60); + this.labelSpatialMatrix.TabIndex = 1000; + this.labelSpatialMatrix.Text = "Spatial Transform Matrix:"; + // + // textBoxSpatialTransformMatrix + // + this.textBoxSpatialTransformMatrix.AcceptsReturn = true; + this.textBoxSpatialTransformMatrix.AcceptsTab = true; + this.textBoxSpatialTransformMatrix.Dock = System.Windows.Forms.DockStyle.Fill; + this.textBoxSpatialTransformMatrix.Location = new System.Drawing.Point(132, 3); + this.textBoxSpatialTransformMatrix.Multiline = true; + this.textBoxSpatialTransformMatrix.Name = "textBoxSpatialTransformMatrix"; + this.textBoxSpatialTransformMatrix.ReadOnly = true; + this.textBoxSpatialTransformMatrix.Size = new System.Drawing.Size(483, 54); + this.textBoxSpatialTransformMatrix.TabIndex = 1000; + this.textBoxSpatialTransformMatrix.TabStop = false; // // statusStrip // this.statusStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { - this.toolStripStatusLabelTS4231, - this.toolStripStatusLabelUser}); - this.statusStrip.Location = new System.Drawing.Point(0, 639); + this.toolStripStatusLabel}); + this.statusStrip.LayoutStyle = System.Windows.Forms.ToolStripLayoutStyle.Flow; + this.statusStrip.Location = new System.Drawing.Point(0, 540); this.statusStrip.Name = "statusStrip"; this.statusStrip.ShowItemToolTips = true; - this.statusStrip.Size = new System.Drawing.Size(604, 22); - this.statusStrip.TabIndex = 8; - this.statusStrip.Text = "statusStrip1"; + this.statusStrip.Size = new System.Drawing.Size(624, 21); + this.statusStrip.TabIndex = 1000; // - // toolStripStatusLabelTS4231 + // toolStripStatusLabel // - this.toolStripStatusLabelTS4231.Image = global::OpenEphys.Onix1.Design.Properties.Resources.StatusBlockedImage; - this.toolStripStatusLabelTS4231.Name = "toolStripStatusLabelTS4231"; - this.toolStripStatusLabelTS4231.Size = new System.Drawing.Size(237, 17); - this.toolStripStatusLabelTS4231.Text = "At least one TS4231 coordinate is invalid."; - this.toolStripStatusLabelTS4231.ToolTipText = "All four TS4231 coordinates must be measured before the spatial transform matrix " + - "can be calculated."; - // - // toolStripStatusLabelUser - // - this.toolStripStatusLabelUser.Image = global::OpenEphys.Onix1.Design.Properties.Resources.StatusBlockedImage; - this.toolStripStatusLabelUser.Name = "toolStripStatusLabelUser"; - this.toolStripStatusLabelUser.Size = new System.Drawing.Size(267, 17); - this.toolStripStatusLabelUser.Text = "At least one user-defined coordinate is invalid."; - this.toolStripStatusLabelUser.ToolTipText = resources.GetString("toolStripStatusLabelUser.ToolTipText"); + this.toolStripStatusLabel.Image = global::OpenEphys.Onix1.Design.Properties.Resources.StatusWarningImage; + this.toolStripStatusLabel.Name = "toolStripStatusLabel"; + this.toolStripStatusLabel.Size = new System.Drawing.Size(221, 16); + this.toolStripStatusLabel.Text = "All fields must be properly populated."; // // SpatialTransformMatrixDialog // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(604, 661); + this.ClientSize = new System.Drawing.Size(624, 561); this.Controls.Add(this.tableLayoutPanelMain); this.Controls.Add(this.statusStrip); this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); - this.MinimumSize = new System.Drawing.Size(620, 700); + this.MinimumSize = new System.Drawing.Size(640, 600); this.Name = "SpatialTransformMatrixDialog"; this.Text = "TS4231V1 Calibration GUI"; this.tableLayoutPanelMain.ResumeLayout(false); this.tableLayoutPanelMain.PerformLayout(); this.groupBoxStatus.ResumeLayout(false); - this.groupBoxStatus.PerformLayout(); this.tableLayoutPanelCoordinates.ResumeLayout(false); this.tableLayoutPanelCoordinates.PerformLayout(); this.flowLayoutPanelBottom.ResumeLayout(false); + this.tableLayoutPanelSpatialMatrix.ResumeLayout(false); + this.tableLayoutPanelSpatialMatrix.PerformLayout(); this.statusStrip.ResumeLayout(false); this.statusStrip.PerformLayout(); this.ResumeLayout(false); @@ -466,33 +625,46 @@ private void InitializeComponent() private System.Windows.Forms.TableLayoutPanel tableLayoutPanelMain; private System.Windows.Forms.GroupBox groupBoxStatus; - private System.Windows.Forms.TextBox textBoxStatus; private System.Windows.Forms.TableLayoutPanel tableLayoutPanelCoordinates; - private System.Windows.Forms.TextBox textBoxUserCoordinate3; - private System.Windows.Forms.TextBox textBoxUserCoordinate2; - private System.Windows.Forms.TextBox textBoxUserCoordinate1; - private System.Windows.Forms.TextBox textBoxUserCoordinate0; - private System.Windows.Forms.TextBox textBoxTS4231Coordinate3; private System.Windows.Forms.TextBox textBoxTS4231Coordinate2; private System.Windows.Forms.TextBox textBoxTS4231Coordinate1; - private System.Windows.Forms.Button buttonMeasure3; private System.Windows.Forms.Button buttonMeasure2; private System.Windows.Forms.Button buttonMeasure1; - private System.Windows.Forms.Label labelCoordinate3; private System.Windows.Forms.Label labelCoordinate2; private System.Windows.Forms.Label labelCoordinate1; private System.Windows.Forms.Button buttonMeasure0; - private System.Windows.Forms.Label labelHeaderTS4231; + private System.Windows.Forms.Label labelTS4231; private System.Windows.Forms.Label labelCoordinate0; private System.Windows.Forms.TextBox textBoxTS4231Coordinate0; - private System.Windows.Forms.Label labelHeaderUser; - private System.Windows.Forms.Button buttonCalculate; private System.Windows.Forms.FlowLayoutPanel flowLayoutPanelBottom; private System.Windows.Forms.Button buttonCancel; private System.Windows.Forms.Label labelInstructions; private System.Windows.Forms.Button buttonOK; private System.Windows.Forms.StatusStrip statusStrip; - private System.Windows.Forms.ToolStripStatusLabel toolStripStatusLabelUser; - private System.Windows.Forms.ToolStripStatusLabel toolStripStatusLabelTS4231; + private System.Windows.Forms.ToolStripStatusLabel toolStripStatusLabel; + private System.Windows.Forms.TextBox textBoxTS4231Coordinate3; + private System.Windows.Forms.Button buttonMeasure3; + private System.Windows.Forms.Label labelCoordinate3; + private System.Windows.Forms.Label labelXyz; + private System.Windows.Forms.TextBox textBoxUserCoordinate0X; + private System.Windows.Forms.Label labelUser; + private System.Windows.Forms.TextBox textBoxUserCoordinate3Z; + private System.Windows.Forms.TextBox textBoxUserCoordinate3Y; + private System.Windows.Forms.TextBox textBoxUserCoordinate3X; + private System.Windows.Forms.TextBox textBoxUserCoordinate2Z; + private System.Windows.Forms.TextBox textBoxUserCoordinate2Y; + private System.Windows.Forms.TextBox textBoxUserCoordinate2X; + private System.Windows.Forms.TextBox textBoxUserCoordinate1Z; + private System.Windows.Forms.TextBox textBoxUserCoordinate1Y; + private System.Windows.Forms.TextBox textBoxUserCoordinate0Z; + private System.Windows.Forms.TextBox textBoxUserCoordinate1X; + private System.Windows.Forms.Label labelY; + private System.Windows.Forms.Label labelX; + private System.Windows.Forms.Label labelZ; + private System.Windows.Forms.TableLayoutPanel tableLayoutPanelSpatialMatrix; + private System.Windows.Forms.Label labelSpatialMatrix; + private System.Windows.Forms.TextBox textBoxSpatialTransformMatrix; + private System.Windows.Forms.RichTextBox richTextBoxStatus; + private System.Windows.Forms.TextBox textBoxUserCoordinate0Y; } } diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs index 04c83a6b..61943691 100644 --- a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs +++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs @@ -4,57 +4,76 @@ using System.Windows.Forms; using System.Reactive.Linq; using Bonsai.Design; +using System.Collections.Generic; +using System.Drawing; namespace OpenEphys.Onix1.Design { /// - /// Partial class to create a spatial-calibration GUI for . + /// Partial class to create a spatial-calibration GUI for . /// public partial class SpatialTransformMatrixDialog : Form { + internal SpatialTransformProperties SpatialTransform; const byte NumMeasurements = 100; - readonly Matrix4x4 inverseM; - - readonly bool[] InputsValid = { false, false, false, false, false, false, false, false }; readonly IObservable PositionDataSource; - readonly Vector3[] TS4231Coordinates = { Vector3.Zero, Vector3.Zero, Vector3.Zero, Vector3.Zero }; - readonly Button[] MeasureButtons; - - IDisposable TextBoxStatusUpdateSubscription; + readonly Vector3[] UserCoordinates = { default, default, default, default }; + readonly Vector3[] TS4231Coordinates = { default, default, default, default }; + Matrix4x4? M; + IDisposable richTextBoxStatusUpdateSubscription; IDisposable MeasurementCalculationSubscription; - internal Matrix4x4 NewSpatialTransform { get; private set; } - - internal SpatialTransformMatrixDialog(IObservable positionDataSource, Matrix4x4 currentM) + internal SpatialTransformMatrixDialog(IObservable dataSource, SpatialTransformProperties transformProperties) { InitializeComponent(); - if (!Matrix4x4.Invert(currentM, out inverseM)) + PositionDataSource = dataSource; + M = transformProperties.M.GetValueOrDefault(); + + Array.Copy(transformProperties.Pre, TS4231Coordinates, 4); + var ts4231TextBoxes = new TextBox[] { + textBoxTS4231Coordinate0, textBoxTS4231Coordinate1, + textBoxTS4231Coordinate2, textBoxTS4231Coordinate3}; + foreach (var (textBox, v) in Enumerable.Zip(ts4231TextBoxes, TS4231Coordinates, (tb, v) => (tb, v))) + textBox.Text = checkVector3ForNaN(v) ? "" : $"{v.X}, {v.Y}, {v.Z}"; + + Array.Copy(transformProperties.Post, UserCoordinates, 4); + var userTextBoxes = new TextBox[] { + textBoxUserCoordinate0X, textBoxUserCoordinate0Y, textBoxUserCoordinate0Z, + textBoxUserCoordinate1X, textBoxUserCoordinate1Y, textBoxUserCoordinate1Z, + textBoxUserCoordinate2X, textBoxUserCoordinate2Y, textBoxUserCoordinate2Z, + textBoxUserCoordinate3X, textBoxUserCoordinate3Y, textBoxUserCoordinate3Z}; + for (byte i = 0; i < 12; i++) { - throw new ArgumentException("Current spatial transform matrix is non-invertible. " + - "You can set M to the identity matrix if you want to start anew."); + ref var component = ref GetComponent(ref UserCoordinates[i / 3], i % 3); + userTextBoxes[i].Text = float.IsNaN(component) ? "" : component.ToString(); } - PositionDataSource = positionDataSource; - MeasureButtons = new Button[] { buttonMeasure0, buttonMeasure1, buttonMeasure2, buttonMeasure3 }; + + CalculatePrintMatrix(); } - private void EnableButtons(bool enable, byte index) + private void TextBoxUserCoordinate_TextChanged(object sender, EventArgs e) { - for (byte i = 0; i < MeasureButtons.Length; i++) - { - MeasureButtons[i].Enabled = enable || (i == index); - } - buttonCalculate.Enabled = enable && InputsValid.All(inputValid => inputValid); + var tag = Convert.ToByte(((TextBox)sender).Tag); + ref var coordinateComponent = ref GetComponent(ref UserCoordinates[tag / 3], tag % 3); + try { coordinateComponent = float.Parse(((TextBox)sender).Text); } + catch { coordinateComponent = float.NaN; } + M = null; + CalculatePrintMatrix(); } private void ButtonMeasure_Click(object sender, EventArgs e) { TextBox[] ts4231TextBoxes = { textBoxTS4231Coordinate0, textBoxTS4231Coordinate1, textBoxTS4231Coordinate2, textBoxTS4231Coordinate3 }; - var index = byte.Parse((string)((Button)sender).Tag); - - if (MeasureButtons[index].Text == "Measure") + var index = Convert.ToByte(((Button)sender).Tag); + ts4231TextBoxes[index].Text = ""; + TS4231Coordinates[index] = new(float.NaN); + if (((Button)sender).Text == "Measure") { - textBoxStatus.AppendText(string.Format("Measuring coordinate {0}...", index) + Environment.NewLine); - MeasureButtons[index].Text = "Cancel"; + richTextBoxStatus.SelectionColor = Color.Blue; + richTextBoxStatus.AppendText($"Measurement at coordinate {index} initiated.\n"); + M = null; + textBoxSpatialTransformMatrix.Text = ""; + ((Button)sender).Text = "Cancel"; EnableButtons(false, index); var sharedPositionDataGroups = PositionDataSource @@ -62,34 +81,30 @@ private void ButtonMeasure_Click(object sender, EventArgs e) .Timeout(new TimeSpan(0, 0, 5), Observable.Empty()) .Publish(); - TextBoxStatusUpdateSubscription = sharedPositionDataGroups + richTextBoxStatusUpdateSubscription = sharedPositionDataGroups .GroupBy(dataFrame => dataFrame.SensorIndex, dataFrame => dataFrame.Position) .SelectMany(group => group.Count().Select(count => new { Index = group.Key, MeasurementCount = count })) .Aggregate( - (TextBoxStatusUpdate: "", Count: 0), + (richTextBoxStatusUpdate: "", Count: 0), (acc, sensor) => { - var textBoxStatusUpdateString = acc.TextBoxStatusUpdate; - textBoxStatusUpdateString += string.Format("{0} measurements from sensor {1}.", - sensor.MeasurementCount, sensor.Index); - textBoxStatusUpdateString += Environment.NewLine; - return (textBoxStatusUpdateString, acc.Count + sensor.MeasurementCount); + var richTextBoxStatusUpdateString = $"{acc.richTextBoxStatusUpdate}{sensor.MeasurementCount} samples from sensor {sensor.Index}.\n"; + return (richTextBoxStatusUpdateString, acc.Count + sensor.MeasurementCount); }, - acc => (acc.TextBoxStatusUpdate, Valid: acc.Count == NumMeasurements)) + acc => (acc.richTextBoxStatusUpdate, Valid: acc.Count == NumMeasurements)) .ObserveOn(new ControlScheduler(this)) .Subscribe(finalResult => { if (finalResult.Valid) { - textBoxStatus.AppendText(finalResult.TextBoxStatusUpdate); - textBoxStatus.AppendText(string.Format("Measurements at coordinate {0} complete.", index) - + Environment.NewLine + Environment.NewLine + "Awaiting user input..." + Environment.NewLine); + richTextBoxStatus.SelectionColor = Color.Black; + richTextBoxStatus.AppendText($"{finalResult.richTextBoxStatusUpdate}Measurement at coordinate {index} complete.\n\n"); } else { - textBoxStatus.AppendText(string.Format("Measurements at coordinate {0} timed out. ", index) - + "Confirm the Lighthouse receivers are within range and unobstructed from Lighthouse transmitters." - + Environment.NewLine + Environment.NewLine + "Awaiting user input..." + Environment.NewLine); + richTextBoxStatus.SelectionColor = Color.Red; + richTextBoxStatus.AppendText($"Measurement at coordinate {index} timed out.\n" + + "Confirm the Lighthouse receivers are within range of and unobstructed from Lighthouse transmitters.\n\n"); } EnableButtons(true, index); }); @@ -100,31 +115,17 @@ private void ButtonMeasure_Click(object sender, EventArgs e) (acc, current) => (acc.Sum + current.Position, acc.Count + 1), acc => { - TS4231Coordinates[index] = Vector3.Transform(acc.Sum / NumMeasurements, inverseM); + TS4231Coordinates[index] = acc.Sum / NumMeasurements; return (Position: TS4231Coordinates[index], Valid: acc.Count == NumMeasurements); }) .ObserveOn(new ControlScheduler(this)) - .Subscribe(finalMeasurement => + .Subscribe(measurement => { - MeasureButtons[index].Text = "Measure"; - if (finalMeasurement.Valid) + ((Button)sender).Text = "Measure"; + if (measurement.Valid) { - ts4231TextBoxes[index].Text = string.Format("{0}, {1}, {2}", - finalMeasurement.Position.X, - finalMeasurement.Position.Y, - finalMeasurement.Position.Z); - InputsValid[index] = true; - if (InputsValid.Take(4).All(ts4231InputValid => ts4231InputValid)) - { - toolStripStatusLabelTS4231.Image = OpenEphys.Onix1.Design.Properties.Resources.StatusReadyImage; - toolStripStatusLabelTS4231.Text = "All TS4231 coordinates are valid."; - } - else - { - toolStripStatusLabelTS4231.Image = OpenEphys.Onix1.Design.Properties.Resources.StatusBlockedImage; - toolStripStatusLabelTS4231.Text = "At least one TS4231 coordinate is invalid."; - } - buttonCalculate.Enabled = InputsValid.All(inputValid => inputValid); + ts4231TextBoxes[index].Text = $"{measurement.Position.X}, {measurement.Position.Y}, {measurement.Position.Z}"; + CalculatePrintMatrix(); } }); @@ -132,66 +133,116 @@ private void ButtonMeasure_Click(object sender, EventArgs e) } else { - TextBoxStatusUpdateSubscription.Dispose(); + richTextBoxStatusUpdateSubscription.Dispose(); MeasurementCalculationSubscription.Dispose(); - textBoxStatus.AppendText(string.Format("Measurements at coordinate {0} cancelled by user.", index) - + Environment.NewLine + Environment.NewLine + "Awaiting user input..." + Environment.NewLine); - MeasureButtons[index].Text = "Measure"; + richTextBoxStatus.SelectionColor = Color.Red; + richTextBoxStatus.AppendText($"Measurement at coordinate {index} cancelled by user.\n\n"); + ((Button)sender).Text = "Measure"; EnableButtons(true, index); } } - private void TextBoxUserCoordinate_TextChanged(object sender, EventArgs e) + private void ButtonOK_Click(object sender, EventArgs e) + { + SpatialTransform = new SpatialTransformProperties(TS4231Coordinates, UserCoordinates, M.GetValueOrDefault()); + if (M == null) + { + var confirmationMessage = ""; + var incompleteInput = false; + if (UserCoordinates.Any(userCoordinate => checkVector3ForNaN(userCoordinate))) + { + incompleteInput = true; + var axes = new char[] { 'X', 'Y', 'Z' }; + var coordinates = new byte[] { 0, 1, 2, 3 }; + confirmationMessage += "At least one coordinate component is empty or invalid:\n"; + for (byte i = 0; i < 12; i++) + { + ref var component = ref GetComponent(ref UserCoordinates[i / 3], i % 3); + if (float.IsNaN(component)) + confirmationMessage += $" • Coordinate {coordinates[i / 3]} {axes[i % 3]} component\n"; + } + confirmationMessage += "\n"; + } + if (TS4231Coordinates.Any(TS4231Coordinate => checkVector3ForNaN(TS4231Coordinate))) + { + incompleteInput = true; + confirmationMessage += "At least one coordinate measurement is empty:\n"; + foreach (var (i, v) in TS4231Coordinates.Select((i, v) => (v, i))) + if (checkVector3ForNaN(v)) + confirmationMessage += $" • Coordinate {i}\n"; + confirmationMessage += "\n"; + } + + if (incompleteInput) + confirmationMessage += "They will not be saved and position data won't properly output.\n\n"; + else if (!Matrix4x4.Invert(Vector3sToMatrix4x4(UserCoordinates), out _)) + confirmationMessage = "The spatial transform matrix is non-invertible " + + "(i.e. not all three axes are spanned in your coordinate selection or some coordinates are repeated). " + + "Position information will be incorrect.\n\n"; + + confirmationMessage += "Would you like to continue?"; + + if (MessageBox.Show(confirmationMessage, "Confirmation", MessageBoxButtons.YesNo) == DialogResult.Yes) + DialogResult = DialogResult.OK; + } + else + DialogResult = DialogResult.OK; + } + + private readonly Func checkVector3ForNaN = v => new[] { v.X, v.Y, v.Z }.Any(float.IsNaN); + + private void EnableButtons(bool enable, byte index) { - var index = int.Parse((string)((TextBox)sender).Tag); - string[] serInputSplit = ((TextBox)sender).Text.Split(','); - InputsValid[index] = serInputSplit.Length == 3 && serInputSplit.All(floatCandidate => float.TryParse(floatCandidate, out _)); - buttonCalculate.Enabled = InputsValid.All(inputValid => inputValid); - if (InputsValid.Skip(4).Take(4).All(userInputValid => userInputValid)) + var buttons = new Button[] { buttonMeasure0, buttonMeasure1, buttonMeasure2, buttonMeasure3, buttonOK, buttonCancel }; + Array.ForEach(buttons, button => button.Enabled = enable || (Convert.ToByte(button.Tag) == index)); + } + + private void CalculatePrintMatrix() + { + if (!UserCoordinates.Any(userCoordinate => checkVector3ForNaN(userCoordinate)) && + !TS4231Coordinates.Any(TS4231Coordinate => checkVector3ForNaN(TS4231Coordinate))) { - toolStripStatusLabelUser.Image = OpenEphys.Onix1.Design.Properties.Resources.StatusReadyImage; - toolStripStatusLabelUser.Text = "All user-defined coordinates are valid."; + if (Matrix4x4.Invert(Vector3sToMatrix4x4(UserCoordinates), out _)) + { + var ts4231V1CoordinatesMatrix = Vector3sToMatrix4x4(TS4231Coordinates); + var userCoordinatesMatrix = Vector3sToMatrix4x4(UserCoordinates); + Matrix4x4.Invert(ts4231V1CoordinatesMatrix, out var ts4231V1CoordinatesMatrixInverted); + M = Matrix4x4.Multiply(ts4231V1CoordinatesMatrixInverted, userCoordinatesMatrix); + textBoxSpatialTransformMatrix.Text = M.Value.ToString(); + toolStripStatusLabel.Image = Properties.Resources.StatusReadyImage; + toolStripStatusLabel.Text = "Spatial transform matrix is calculated."; + } + else + { + toolStripStatusLabel.Image = Properties.Resources.StatusWarningImage; + toolStripStatusLabel.Text = "The resulting spatial transform matrix must be non-invertible."; + } } else { - toolStripStatusLabelUser.Image = OpenEphys.Onix1.Design.Properties.Resources.StatusBlockedImage; - toolStripStatusLabelUser.Text = "At least one user-defined coordinate is invalid."; + toolStripStatusLabel.Image = Properties.Resources.StatusWarningImage; + toolStripStatusLabel.Text = "All fields must be properly populated."; } } - private void ButtonCalculate_Click(object sender, EventArgs e) + private static ref float GetComponent(ref Vector3 v, int index) { - var ts4231V1CoordinatesMatrix = new Matrix4x4( - TS4231Coordinates[0].X, TS4231Coordinates[0].Y, TS4231Coordinates[0].Z, 1, - TS4231Coordinates[1].X, TS4231Coordinates[1].Y, TS4231Coordinates[1].Z, 1, - TS4231Coordinates[2].X, TS4231Coordinates[2].Y, TS4231Coordinates[2].Z, 1, - TS4231Coordinates[3].X, TS4231Coordinates[3].Y, TS4231Coordinates[3].Z, 1); - - float[][] userCoordinates = { - textBoxUserCoordinate0.Text.Split(',').Select(item => float.Parse(item)).ToArray(), - textBoxUserCoordinate1.Text.Split(',').Select(item => float.Parse(item)).ToArray(), - textBoxUserCoordinate2.Text.Split(',').Select(item => float.Parse(item)).ToArray(), - textBoxUserCoordinate3.Text.Split(',').Select(item => float.Parse(item)).ToArray()}; - - var userCoordinatesMatrix = new Matrix4x4( - userCoordinates[0][0], userCoordinates[0][1], userCoordinates[0][2], 1, - userCoordinates[1][0], userCoordinates[1][1], userCoordinates[1][2], 1, - userCoordinates[2][0], userCoordinates[2][1], userCoordinates[2][2], 1, - userCoordinates[3][0], userCoordinates[3][1], userCoordinates[3][2], 1); - - Matrix4x4.Invert(ts4231V1CoordinatesMatrix, out var ts4231V1CoordinatesMatrixInverted); - NewSpatialTransform = Matrix4x4.Multiply(ts4231V1CoordinatesMatrixInverted, userCoordinatesMatrix); - - textBoxStatus.AppendText("The spatial transform matrix for the above coordinates is:" + Environment.NewLine); - textBoxStatus.AppendText(NewSpatialTransform.ToString() + Environment.NewLine + Environment.NewLine); - textBoxStatus.AppendText("Awaiting user input..." + Environment.NewLine); - - buttonOK.Enabled = true; + switch (index) + { + case 0: return ref v.X; + case 1: return ref v.Y; + case 2: return ref v.Z; + default: throw new IndexOutOfRangeException(); + }; } - private void ButtonOKOrCancel_Click(object sender, EventArgs e) + private Matrix4x4 Vector3sToMatrix4x4(IList rows) { - Close(); + return new Matrix4x4( + rows[0].X, rows[0].Y, rows[0].Z, 1, + rows[1].X, rows[1].Y, rows[1].Z, 1, + rows[2].X, rows[2].Y, rows[2].Z, 1, + rows[3].X, rows[3].Y, rows[3].Z, 1); } } } diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.resx b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.resx index f9416718..fabd65ad 100644 --- a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.resx +++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.resx @@ -118,21 +118,21 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Follow the instructions below to transform naive TS4231 position data from the base-station reference frame to a user-defined reference frame. For more in-depth instructions, find the corresponding tutorial in Open Ephys' online documentation. -1) For each coordinate: - • Place the TS4231V1 device and click the corresponding "Measure" button. - • Input how you would like to define the coordinate in the user-defined reference frame. -2) Click the "Calculate Spatial Transform" button which enables after all fields are populated with valid data. -3) Click "OK" to close this GUI and set the M property as the calculated spatial transform matrix. Click "Cancel" to close this GUI and not set the M property. + Follow the instructions below to transform TS4231 position data from a generic base-station reference frame to a user-defined reference frame: + +1) For each coordinate: + • Place the TS4231V1 device and click the "Measure" button. + • Define the coordinate in the user-defined reference frame using the fields under "User Coordinates". +2) Click "OK" to close this GUI and set the spatial transform properties in the workflow. + +For more in-depth instructions, find the corresponding tutorial in Open Ephys' online documentation. - 36, 14 + -2, 3 + + + 81 - - All four user-defined coordinates must have the following format: "XX, YY, ZZ" or -"XX.XX, YY.YY, ZZ.ZZ" with any number of digits following the decimal before the -spatial transform matrix can be calculated. - diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs b/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs index 4a1e2d7c..bce7883a 100644 --- a/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs +++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs @@ -5,10 +5,8 @@ using System.Windows.Forms.Design; using System.Windows.Forms; using System.Reactive.Linq; -using System.Numerics; using Bonsai.Design; - namespace OpenEphys.Onix1.Design { /// @@ -18,10 +16,7 @@ namespace OpenEphys.Onix1.Design public class SpatialTransformMatrixEditor : DataSourceTypeEditor { /// - public SpatialTransformMatrixEditor() - : base(DataSource.Output, typeof(void)) - { - } + public SpatialTransformMatrixEditor() : base(DataSource.Input, typeof(void)) { } /// public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context) @@ -38,14 +33,14 @@ public override object EditValue(ITypeDescriptorContext context, IServiceProvide { var source = GetDataSource(context, provider); var dataFrames = source.Output.Merge().Select(x => x as TS4231V1PositionDataFrame); - using var visualizerDialog = new SpatialTransformMatrixDialog(dataFrames, (Matrix4x4)value); + using var visualizerDialog = new SpatialTransformMatrixDialog(dataFrames, (SpatialTransformProperties)value); if (!editorState.WorkflowRunning) { throw new InvalidOperationException("Workflow must be running to open this GUI."); } else if (editorService.ShowDialog(visualizerDialog) == DialogResult.OK) { - return visualizerDialog.NewSpatialTransform; + return visualizerDialog.SpatialTransform; } } return base.EditValue(context, provider, value); diff --git a/OpenEphys.Onix1/OpenEphys.Onix1.csproj b/OpenEphys.Onix1/OpenEphys.Onix1.csproj index cf6b5a4b..24e6912a 100644 --- a/OpenEphys.Onix1/OpenEphys.Onix1.csproj +++ b/OpenEphys.Onix1/OpenEphys.Onix1.csproj @@ -10,6 +10,10 @@ True + + + + diff --git a/OpenEphys.Onix1/TS4231V1PositionData.cs b/OpenEphys.Onix1/TS4231V1PositionData.cs index e8a34e33..42450b30 100644 --- a/OpenEphys.Onix1/TS4231V1PositionData.cs +++ b/OpenEphys.Onix1/TS4231V1PositionData.cs @@ -42,7 +42,6 @@ namespace OpenEphys.Onix1 /// using downstream processing. /// /// - [DefaultProperty(nameof(M))] [Description("Produces a sequence of 3D positions from an array of Triad Semiconductor TS4231 receivers beneath a pair of SteamVR V1 base stations.")] public class TS4231V1PositionData : Source { @@ -75,20 +74,6 @@ public class TS4231V1PositionData : Source [Category(DeviceFactory.ConfigurationCategory)] public Point3d Q { get; set; } = new(1, 0, 0); - - /// - /// Gets or sets a spatial transform to convert position measurements to an external coordinate - /// system. - /// - [Description("Spatial transform matrix to convert position measurements to an external coordinate system.")] - [Category(DeviceFactory.ConfigurationCategory)] - [Editor("OpenEphys.Onix1.Design.SpatialTransformMatrixEditor, OpenEphys.Onix1.Design", DesignTypes.UITypeEditor)] - [TypeConverter(typeof(NumericRecordConverter))] - public Matrix4x4 M { get; set; } = new Matrix4x4( 1, 0, 0, 0, - 0, 1, 0, 0, - 0, 0, 1, 0, - 0, 0, 0, 1); - /// /// Generates a sequence of objects, each of which contains /// the 3D position of single photodiode. @@ -119,7 +104,7 @@ public unsafe override IObservable Generate() .GetDeviceFrames(device.Address) .SubscribeSafe(frameObserver); })) - .Select(input => new TS4231V1PositionDataFrame(input.Clock, input.HubClock, input.SensorIndex, Vector3.Transform(input.Position, M))); + .Select(input => new TS4231V1PositionDataFrame(input.Clock, input.HubClock, input.SensorIndex, input.Position)); } } } diff --git a/OpenEphys.Onix1/TS4231V1SpatialTransform.cs b/OpenEphys.Onix1/TS4231V1SpatialTransform.cs new file mode 100644 index 00000000..520f4c30 --- /dev/null +++ b/OpenEphys.Onix1/TS4231V1SpatialTransform.cs @@ -0,0 +1,89 @@ +using System; +using System.ComponentModel; +using System.Numerics; +using System.Reactive.Linq; +using Bonsai; + +namespace OpenEphys.Onix1 +{ + /// + /// Transforms a sequence of 3D positions from to an external coordinate system. + /// + [DefaultProperty(nameof(SpatialTransform))] + public class TS4231V1SpatialTransform : Transform + { + /// + /// Gets or sets the pre- and post- transform coordinates to calculate + /// the spatial transform matrix as well as the spatial transform matrix + /// itself. + /// + [Editor("OpenEphys.Onix1.Design.SpatialTransformMatrixEditor, OpenEphys.Onix1.Design", DesignTypes.UITypeEditor)] + [Description("Data for transforming position measurements to another reference frame.")] + public SpatialTransformProperties SpatialTransform { get; set; } = new(); + + /// + /// Transforms a sequence of + /// objects, each of which contains transformed 3D position of single + /// photodiode. + /// + /// + /// A sequence of objects with + /// transformed position data. + /// + public override IObservable Process(IObservable source) + { + return source.Select(input => + new TS4231V1PositionDataFrame(input.Clock, input.HubClock, input.SensorIndex, + Vector3.Transform(input.Position, SpatialTransform.M.GetValueOrDefault()))); + } + } + + /// + /// Data necessary to construct a spatial transform matrix as well as the + /// spatial transform matrix itself. + /// + public readonly record struct SpatialTransformProperties + { + /// + /// A set of coodinates before undergoing a spatial transform. + /// + public readonly Vector3[] Pre = { new(float.NaN), new(float.NaN), new(float.NaN), new(float.NaN) }; + /// + /// A set of coodinates after undergoing a spatial transform. + /// + public readonly Vector3[] Post = { new(float.NaN), new(float.NaN), new(float.NaN), new(float.NaN) }; + /// + /// The spatial transform matrix calculated from and . + /// + public readonly Matrix4x4? M = null; + + /// + /// Initializes a new instance of the struct with default values. + /// + public SpatialTransformProperties() { } + + /// + /// Initializes a new instance of the with values specified using + /// parameters. + /// + /// + /// The value used to set . + /// + /// + /// The value used to set . + /// + /// + /// The value used to set . + /// + public SpatialTransformProperties(Vector3[] pre, Vector3[] post, Matrix4x4 m) + { + Array.Copy(pre, Pre, 4); + Array.Copy(post, Post, 4); + M = m; + } + } +} diff --git a/OpenEphys.Onix1/TS4231V1TransformedPositionData.bonsai b/OpenEphys.Onix1/TS4231V1TransformedPositionData.bonsai new file mode 100644 index 00000000..9a8a7a35 --- /dev/null +++ b/OpenEphys.Onix1/TS4231V1TransformedPositionData.bonsai @@ -0,0 +1,65 @@ + + + + + + + + + + + 0 + 0 + 0 + + + 1 + 0 + 0 + + + 1 + 0 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 0 + 1 + + 0 + 0 + 0 + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spatial-transform_issue-427.patch b/spatial-transform_issue-427.patch new file mode 100644 index 00000000..b31e8ac2 --- /dev/null +++ b/spatial-transform_issue-427.patch @@ -0,0 +1,599 @@ +diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs +index c5530bf..b0dcef2 100644 +--- a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs ++++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs +@@ -1,11 +1,10 @@ + using System; ++using System.Drawing; + using System.Linq; + using System.Numerics; +-using System.Windows.Forms; + using System.Reactive.Linq; ++using System.Windows.Forms; + using Bonsai.Design; +-using System.Collections.Generic; +-using System.Drawing; + + namespace OpenEphys.Onix1.Design + { +@@ -14,58 +13,61 @@ namespace OpenEphys.Onix1.Design + /// + public partial class SpatialTransformMatrixDialog : Form + { +- internal SpatialTransformProperties SpatialTransform; ++ internal SpatialTransform3D SpatialTransform; + const byte NumMeasurements = 100; + readonly IObservable PositionDataSource; + IDisposable richTextBoxStatusUpdateSubscription; + IDisposable MeasurementCalculationSubscription; + +- internal SpatialTransformMatrixDialog(IObservable dataSource, SpatialTransformProperties transformProperties) ++ internal SpatialTransformMatrixDialog(IObservable dataSource, SpatialTransform3D transformProperties) + { + InitializeComponent(); + SpatialTransform = transformProperties; + PositionDataSource = dataSource; + +- var ts4231TextBoxes = new TextBox[] { +- textBoxTS4231Coordinate0, textBoxTS4231Coordinate1, +- textBoxTS4231Coordinate2, textBoxTS4231Coordinate3}; +- foreach (var (textBox, v) in Enumerable.Zip(ts4231TextBoxes, SpatialTransform.Pre, (tb, v) => (tb, v))) +- textBox.Text = checkVector3ForNaN(v) ? "" : $"{v.X}, {v.Y}, {v.Z}"; ++ var ts4231TextBoxes = new TextBox[] { ++ textBoxTS4231Coordinate0, textBoxTS4231Coordinate1, ++ textBoxTS4231Coordinate2, textBoxTS4231Coordinate3 }; ++ var preTransformCoordinates = SpatialTransform.MatrixToFloatArray(SpatialTransform.A); ++ for (byte i = 0; i < 3; i++) ++ ts4231TextBoxes[i].Text = float.IsNaN(preTransformCoordinates[i * 3]) ? "" : $"{preTransformCoordinates[i * 3]}, " + ++ $"{preTransformCoordinates[i * 3 + 1]}, " + ++ $"{preTransformCoordinates[i * 3 + 2]}"; + + var userTextBoxes = new TextBox[] { + textBoxUserCoordinate0X, textBoxUserCoordinate0Y, textBoxUserCoordinate0Z, + textBoxUserCoordinate1X, textBoxUserCoordinate1Y, textBoxUserCoordinate1Z, + textBoxUserCoordinate2X, textBoxUserCoordinate2Y, textBoxUserCoordinate2Z, +- textBoxUserCoordinate3X, textBoxUserCoordinate3Y, textBoxUserCoordinate3Z}; +- for (byte i = 0; i < 12; i++) +- { +- ref var component = ref GetComponent(ref SpatialTransform.Post[i / 3], i % 3); +- userTextBoxes[i].Text = float.IsNaN(component) ? "" : component.ToString(); +- } ++ textBoxUserCoordinate3X, textBoxUserCoordinate3Y, textBoxUserCoordinate3Z }; ++ var postTransformCoordinates = SpatialTransform.MatrixToFloatArray(SpatialTransform.B); ++ foreach (var (tb, comp) in Enumerable.Zip(userTextBoxes, postTransformCoordinates, (tb, comp) => (tb, comp))) ++ tb.Text = float.IsNaN(comp) ? "" : comp.ToString(); + +- CalculatePrintMatrix(); ++ IndicateSpatialTransformStatus(); + } + + private void TextBoxUserCoordinate_TextChanged(object sender, EventArgs e) + { + var tag = Convert.ToByte(((TextBox)sender).Tag); +- ref var coordinateComponent = ref GetComponent(ref SpatialTransform.Post[tag / 3], tag % 3); +- try { coordinateComponent = float.Parse(((TextBox)sender).Text); } +- catch { coordinateComponent = float.NaN; } +- CalculatePrintMatrix(); ++ try { SpatialTransform.SetMatrixBElement(float.Parse(((TextBox)sender).Text), tag / 3, tag % 3); } ++ catch { SpatialTransform.SetMatrixBElement(float.NaN, tag / 3, tag % 3); } ++ IndicateSpatialTransformStatus(); + } + + private void ButtonMeasure_Click(object sender, EventArgs e) + { + TextBox[] ts4231TextBoxes = { textBoxTS4231Coordinate0, textBoxTS4231Coordinate1, textBoxTS4231Coordinate2, textBoxTS4231Coordinate3 }; + var index = Convert.ToByte(((Button)sender).Tag); ++ ++ for (byte i = 0; i < 3; i++) ++ SpatialTransform.SetMatrixAElement(float.NaN, index, i); + ts4231TextBoxes[index].Text = ""; +- SpatialTransform.Pre[index] = new(float.NaN); ++ + if (((Button)sender).Text == "Measure") + { + richTextBoxStatus.SelectionColor = Color.Blue; + richTextBoxStatus.AppendText($"Measurement at coordinate {index} initiated.\n"); +- SpatialTransform.M = null; ++ IndicateSpatialTransformStatus(); + textBoxSpatialTransformMatrix.Text = ""; + ((Button)sender).Text = "Cancel"; + EnableButtons(false, index); +@@ -109,8 +111,12 @@ namespace OpenEphys.Onix1.Design + (acc, current) => (acc.Sum + current.Position, acc.Count + 1), + acc => + { +- SpatialTransform.Pre[index] = acc.Sum / NumMeasurements; +- return (Position: SpatialTransform.Pre[index], Valid: acc.Count == NumMeasurements); ++ var measurement = acc.Sum / NumMeasurements; ++ SpatialTransform.SetMatrixAElement(measurement.X, index, 0); ++ SpatialTransform.SetMatrixAElement(measurement.Y, index, 1); ++ SpatialTransform.SetMatrixAElement(measurement.Z, index, 2); ++ Console.WriteLine(SpatialTransform.A.ToString()); ++ return (Position: measurement, Valid: acc.Count == NumMeasurements); + }) + .ObserveOn(new ControlScheduler(this)) + .Subscribe(measurement => +@@ -119,7 +125,7 @@ namespace OpenEphys.Onix1.Design + if (measurement.Valid) + { + ts4231TextBoxes[index].Text = $"{measurement.Position.X}, {measurement.Position.Y}, {measurement.Position.Z}"; +- CalculatePrintMatrix(); ++ IndicateSpatialTransformStatus(); + } + }); + +@@ -138,106 +144,69 @@ namespace OpenEphys.Onix1.Design + + private void ButtonOK_Click(object sender, EventArgs e) + { +- if (SpatialTransform.M.HasValue) +- DialogResult = DialogResult.OK; +- else ++ var confirmationMessage = ""; ++ var invalidInput = false; ++ if (SpatialTransform.ContainsNaN(SpatialTransform.A) || SpatialTransform.ContainsNaN(SpatialTransform.B)) + { +- var confirmationMessage = ""; +- var incompleteInput = false; +- if (SpatialTransform.Post.Any(userCoordinate => checkVector3ForNaN(userCoordinate))) +- { +- incompleteInput = true; +- var axes = new char[] { 'X', 'Y', 'Z' }; +- var coordinates = new byte[] { 0, 1, 2, 3 }; +- confirmationMessage += "At least one coordinate component is empty or invalid:\n"; +- for (byte i = 0; i < 12; i++) +- { +- ref var component = ref GetComponent(ref SpatialTransform.Post[i / 3], i % 3); +- if (float.IsNaN(component)) +- confirmationMessage += $" • Coordinate {coordinates[i / 3]} {axes[i % 3]} component\n"; +- } +- confirmationMessage += "\n"; +- } +- if (SpatialTransform.Pre.Any(TS4231Coordinate => checkVector3ForNaN(TS4231Coordinate))) +- { +- incompleteInput = true; +- confirmationMessage += "At least one coordinate measurement is empty:\n"; +- foreach (var (i, v) in SpatialTransform.Pre.Select((i, v) => (v, i))) +- if (checkVector3ForNaN(v)) +- confirmationMessage += $" • Coordinate {i}\n"; +- confirmationMessage += "\n"; +- } +- +- if (incompleteInput) +- confirmationMessage += "They will not be saved and transformed position data won't be properly output.\n\n"; +- else if (!Matrix4x4.Invert(Vector3sToMatrix4x4(SpatialTransform.Post), out _)) +- confirmationMessage = "The spatial transform matrix is non-invertible. The transformed position data won't be properly output.\n\n"; +- +- confirmationMessage += "Would you like to continue?"; ++ confirmationMessage = $"At least one entry in the {Name} is invalid for calculating a proper 3D spatial transform:\n"; ++ ++ var axes = new char[] { 'X', 'Y', 'Z' }; ++ var coordinates = new byte[] { 0, 1, 2, 3 }; ++ ++ for (byte i = 0; i < 12; i++) ++ if (float.IsNaN(SpatialTransform.MatrixToFloatArray(SpatialTransform.B)[i])) ++ confirmationMessage += $" • Component {axes[i % 3]} from user coordinate {coordinates[i / 3]}\n"; + ++ for (byte i = 0; i < 4; i++) ++ if (float.IsNaN(SpatialTransform.MatrixToFloatArray(SpatialTransform.A)[i * 3])) ++ confirmationMessage += $" • TS4231 Coordinate {i}\n"; ++ ++ confirmationMessage += "\nThese invalid entries will not be saved. "; ++ invalidInput = true; ++ } ++ else if (!Matrix4x4.Invert(SpatialTransform.M, out _)) ++ { ++ confirmationMessage = $"The calculated spatial transform matrix is non-invertible\n"; ++ invalidInput = true; ++ } ++ ++ if (invalidInput) ++ { ++ confirmationMessage += "The transformed position data will be zeros until these entries are fixed.\n\n" + ++ "Would you like to continue?"; + if (MessageBox.Show(confirmationMessage, "Confirmation", MessageBoxButtons.YesNo) == DialogResult.Yes) + DialogResult = DialogResult.OK; +- } ++ } ++ else ++ DialogResult = DialogResult.OK; + } + +- private readonly Func checkVector3ForNaN = v => new[] { v.X, v.Y, v.Z }.Any(float.IsNaN); +- + private void EnableButtons(bool enable, byte index) + { + var buttons = new Button[] { buttonMeasure0, buttonMeasure1, buttonMeasure2, buttonMeasure3, buttonOK, buttonCancel }; + Array.ForEach(buttons, button => button.Enabled = enable || (Convert.ToByte(button.Tag) == index)); + } + +- private void CalculatePrintMatrix() ++ private void IndicateSpatialTransformStatus() + { +- SpatialTransform.M = null; +- if (!SpatialTransform.Post.Any(userCoordinate => checkVector3ForNaN(userCoordinate)) && +- !SpatialTransform.Pre.Any(TS4231Coordinate => checkVector3ForNaN(TS4231Coordinate))) +- { +- if (Matrix4x4.Invert(Vector3sToMatrix4x4(SpatialTransform.Post), out _)) +- { +- var ts4231V1CoordinatesMatrix = Vector3sToMatrix4x4(SpatialTransform.Pre); +- var userCoordinatesMatrix = Vector3sToMatrix4x4(SpatialTransform.Post); +- Matrix4x4.Invert(ts4231V1CoordinatesMatrix, out var ts4231V1CoordinatesMatrixInverted); +- SpatialTransform.M = Matrix4x4.Multiply(ts4231V1CoordinatesMatrixInverted, userCoordinatesMatrix); +- toolStripStatusLabel.Image = Properties.Resources.StatusReadyImage; +- toolStripStatusLabel.Text = "Spatial transform matrix is calculated."; +- } +- else +- { +- toolStripStatusLabel.Image = Properties.Resources.StatusWarningImage; +- toolStripStatusLabel.Text = "The resulting spatial transform matrix must be non-invertible."; +- } +- } +- else ++ if (SpatialTransform.ContainsNaN(SpatialTransform.A) || SpatialTransform.ContainsNaN(SpatialTransform.B)) + { + toolStripStatusLabel.Image = Properties.Resources.StatusWarningImage; + toolStripStatusLabel.Text = "All fields must be properly populated."; ++ textBoxSpatialTransformMatrix.Text = ""; + } +- if (SpatialTransform.M.HasValue) +- textBoxSpatialTransformMatrix.Text = SpatialTransform.M.Value.ToString(); +- else ++ else if (!Matrix4x4.Invert(SpatialTransform.M, out _)) ++ { ++ toolStripStatusLabel.Image = Properties.Resources.StatusWarningImage; ++ toolStripStatusLabel.Text = "The calculated spatial transform matrix must be invertible."; + textBoxSpatialTransformMatrix.Text = ""; +- } +- +- private static ref float GetComponent(ref Vector3 v, int index) +- { +- switch (index) ++ } ++ else + { +- case 0: return ref v.X; +- case 1: return ref v.Y; +- case 2: return ref v.Z; +- default: throw new IndexOutOfRangeException(); +- }; +- } +- +- private Matrix4x4 Vector3sToMatrix4x4(IList rows) +- { +- return new Matrix4x4( +- rows[0].X, rows[0].Y, rows[0].Z, 1, +- rows[1].X, rows[1].Y, rows[1].Z, 1, +- rows[2].X, rows[2].Y, rows[2].Z, 1, +- rows[3].X, rows[3].Y, rows[3].Z, 1); ++ toolStripStatusLabel.Image = Properties.Resources.StatusReadyImage; ++ toolStripStatusLabel.Text = "Spatial transform matrix is calculated."; ++ textBoxSpatialTransformMatrix.Text = SpatialTransform.M.ToString(); ++ } + } + } + } +diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs b/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs +index 66f8bca..6be7ac3 100644 +--- a/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs ++++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs +@@ -33,7 +33,7 @@ namespace OpenEphys.Onix1.Design + { + var source = GetDataSource(context, provider); + var dataFrames = source.Output.Merge().Select(x => x as TS4231V1PositionDataFrame); +- using var visualizerDialog = new SpatialTransformMatrixDialog(dataFrames, new SpatialTransformProperties((SpatialTransformProperties)value)); ++ using var visualizerDialog = new SpatialTransformMatrixDialog(dataFrames, new SpatialTransform3D((SpatialTransform3D)value)); + if (!editorState.WorkflowRunning) + { + throw new InvalidOperationException("Workflow must be running to open this GUI."); +diff --git a/OpenEphys.Onix1/SpatialTransform3D.cs b/OpenEphys.Onix1/SpatialTransform3D.cs +new file mode 100644 +index 0000000..4b9c2b8 +--- /dev/null ++++ b/OpenEphys.Onix1/SpatialTransform3D.cs +@@ -0,0 +1,122 @@ ++using System; ++using System.Linq; ++using System.Numerics; ++using System.Xml.Serialization; ++ ++namespace OpenEphys.Onix1 ++{ ++ /// ++ /// Data necessary to construct a spatial transform matrix as well as the ++ /// spatial transform matrix itself. ++ /// ++ public class SpatialTransform3D ++ { ++ ++ private Matrix4x4 _a, _b; ++ ++ /// ++ /// The A matrix in A * = . It is ++ /// constructed from a set of four Cartesian coordinates before ++ /// undergoing a spatial transformation. ++ /// ++ public Matrix4x4 A { get => _a; set { _a = value; UpdateM(); } } ++ ++ /// ++ /// The B matrix in * = B. It is ++ /// constructed from a set of four Cartesian coordinates after ++ /// undergoing a spatial transformation. ++ /// ++ public Matrix4x4 B { get => _b ; set { _b = value; UpdateM(); } } ++ ++ /// ++ /// The M matrix in * = M. It is the ++ /// spatial transform matrix. It calculated as M = A.inv * B. ++ /// ++ [XmlIgnore] ++ public Matrix4x4 M { get; private set; } ++ ++ /// ++ /// Initializes a new instance of the ++ /// class with default values. ++ /// ++ public SpatialTransform3D() ++ { ++ A = B = new(float.NaN, float.NaN, float.NaN, 1, ++ float.NaN, float.NaN, float.NaN, 1, ++ float.NaN, float.NaN, float.NaN, 1, ++ float.NaN, float.NaN, float.NaN, 1); ++ M = new(float.NaN, float.NaN, float.NaN, float.NaN, ++ float.NaN, float.NaN, float.NaN, float.NaN, ++ float.NaN, float.NaN, float.NaN, float.NaN, ++ float.NaN, float.NaN, float.NaN, float.NaN); ++ } ++ ++ /// ++ /// Initializes a new instance of the ++ /// class as a copy of an existing instance. ++ /// ++ /// The instance to copy. ++ public SpatialTransform3D(SpatialTransform3D other) ++ { ++ A = other.A; ++ B = other.B; ++ } ++ ++ /// ++ /// Sets a component (X, Y, or Z) in one of the coordinates in ++ /// PreTransformCoordinates. ++ /// ++ public void SetMatrixAElement(float value, int coordinate, int component) => ++ SetMatrixElement(ref _a, value, coordinate, component); ++ ++ /// ++ /// Sets a component (X, Y, or Z) in one of the coordinates in ++ /// PostTransformCoordinates. ++ /// ++ public void SetMatrixBElement(float value, int coordinate, int component) => ++ SetMatrixElement(ref _b, value, coordinate, component); ++ ++ private void SetMatrixElement(ref Matrix4x4 m, float value, int coordinate, int component) ++ { ++ if (coordinate is < 0 or > 3) throw new ArgumentOutOfRangeException(nameof(coordinate) + " must be 0, 1, 2, or 3."); ++ if (component is < 0 or > 2) throw new ArgumentOutOfRangeException(nameof(component) + " must be 0, 1, or 2."); ++ ++ switch ((coordinate, component)) ++ { ++ case (0, 0): m.M11 = value; break; ++ case (0, 1): m.M12 = value; break; ++ case (0, 2): m.M13 = value; break; ++ case (1, 0): m.M21 = value; break; ++ case (1, 1): m.M22 = value; break; ++ case (1, 2): m.M23 = value; break; ++ case (2, 0): m.M31 = value; break; ++ case (2, 1): m.M32 = value; break; ++ case (2, 2): m.M33 = value; break; ++ case (3, 0): m.M41 = value; break; ++ case (3, 1): m.M42 = value; break; ++ case (3, 2): m.M43 = value; break; ++ } ++ UpdateM(); ++ } ++ ++ private void UpdateM() ++ { ++ Matrix4x4.Invert(A, out var AInverted); ++ M = Matrix4x4.Multiply(AInverted, B); ++ } ++ ++ /// ++ /// Convert coordinates from matrix to a float array. ++ /// ++ public float[] MatrixToFloatArray(Matrix4x4 m) => ++ new float[] { m.M11, m.M12, m.M13, ++ m.M21, m.M22, m.M23, ++ m.M31, m.M32, m.M33, ++ m.M41, m.M42, m.M43 }; ++ ++ /// ++ /// Checks if matrix contains one or more NaNs. ++ /// ++ public bool ContainsNaN(Matrix4x4 m) => MatrixToFloatArray(m).Any(float.IsNaN); ++ } ++} +diff --git a/OpenEphys.Onix1/SpatialTransformProperties.cs b/OpenEphys.Onix1/SpatialTransformProperties.cs +deleted file mode 100644 +index f20306b..0000000 +--- a/OpenEphys.Onix1/SpatialTransformProperties.cs ++++ /dev/null +@@ -1,58 +0,0 @@ +-using System; +-using System.Collections.Generic; +-using System.Linq; +-using System.Numerics; +-using System.Text; +-using System.Threading.Tasks; +- +-namespace OpenEphys.Onix1 +-{ +- /// +- /// Data necessary to construct a spatial transform matrix as well as the +- /// spatial transform matrix itself. +- /// +- public class SpatialTransformProperties +- { +- /// +- /// The set of coordinates before undergoing a spatial transform. +- /// +- public Vector3[] Pre { get; set; } +- +- /// +- /// The set of coordinates after undergoing a spatial transform. +- /// +- public Vector3[] Post { get; set; } +- +- /// +- /// The spatial transform matrix calculated from and +- /// . +- /// +- public Matrix4x4? M { get; set; } +- +- /// +- /// Initializes a new instance of the class with default values. +- /// +- public SpatialTransformProperties() +- { +- Pre = new Vector3[] { new(float.NaN), new(float.NaN), new(float.NaN), new(float.NaN) }; +- Post = new Vector3[] { new(float.NaN), new(float.NaN), new(float.NaN), new(float.NaN) }; +- M = null; +- } +- +- /// +- /// Initializes a new instance of the class as a copy of an existing +- /// instance. +- /// +- /// The instance to copy. +- public SpatialTransformProperties(SpatialTransformProperties other) +- { +- Pre = new Vector3[4]; +- Post = new Vector3[4]; +- Array.Copy(other.Pre, Pre, 4); +- Array.Copy(other.Post, Post, 4); +- M = other.M; +- } +- } +-} +diff --git a/OpenEphys.Onix1/TS4231V1SpatialTransform.cs b/OpenEphys.Onix1/TS4231V1SpatialTransform.cs +index 7176f6d..7475130 100644 +--- a/OpenEphys.Onix1/TS4231V1SpatialTransform.cs ++++ b/OpenEphys.Onix1/TS4231V1SpatialTransform.cs +@@ -19,7 +19,7 @@ namespace OpenEphys.Onix1 + /// + [Editor("OpenEphys.Onix1.Design.SpatialTransformMatrixEditor, OpenEphys.Onix1.Design", DesignTypes.UITypeEditor)] + [Description("Data for transforming position measurements to another reference frame.")] +- public SpatialTransformProperties SpatialTransform { get; set; } = new(); ++ public SpatialTransform3D SpatialTransform { get; set; } = new(); + + /// + /// Transforms a sequence of +@@ -34,7 +34,7 @@ namespace OpenEphys.Onix1 + { + return source.Select(input => + new TS4231V1PositionDataFrame(input.Clock, input.HubClock, input.SensorIndex, +- Vector3.Transform(input.Position, SpatialTransform.M.GetValueOrDefault()))); ++ Vector3.Transform(input.Position, SpatialTransform.M))); + } + } + } +diff --git a/OpenEphys.Onix1/TS4231V1TransformedPositionData.bonsai b/OpenEphys.Onix1/TS4231V1TransformedPositionData.bonsai +index 9a8a7a3..60dd9e2 100644 +--- a/OpenEphys.Onix1/TS4231V1TransformedPositionData.bonsai ++++ b/OpenEphys.Onix1/TS4231V1TransformedPositionData.bonsai +@@ -1,5 +1,5 @@ +  +- +@@ -20,29 +20,6 @@ + 0 + 0 + +- +- 1 +- 0 +- 0 +- 0 +- 0 +- 1 +- 0 +- 0 +- 0 +- 0 +- 1 +- 0 +- 0 +- 0 +- 0 +- 1 +- +- 0 +- 0 +- 0 +- +- + + + +@@ -50,7 +27,54 @@ + + + +- ++ ++ ++ NaN ++ NaN ++ NaN ++ 1 ++ NaN ++ NaN ++ NaN ++ 1 ++ NaN ++ NaN ++ NaN ++ 1 ++ NaN ++ NaN ++ NaN ++ 1 ++ ++ NaN ++ NaN ++ NaN ++ ++ ++ ++ NaN ++ NaN ++ NaN ++ 1 ++ NaN ++ NaN ++ NaN ++ 1 ++ NaN ++ NaN ++ NaN ++ 1 ++ NaN ++ NaN ++ NaN ++ 1 ++ ++ NaN ++ NaN ++ NaN ++ ++ ++ + + + From d7a13163deee20958d5726dbaa9f36debbfe43e3 Mon Sep 17 00:00:00 2001 From: cjsha Date: Thu, 12 Jun 2025 10:48:39 -0400 Subject: [PATCH 09/17] Revert TS4231V1PositionData --- OpenEphys.Onix1/TS4231V1PositionData.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/OpenEphys.Onix1/TS4231V1PositionData.cs b/OpenEphys.Onix1/TS4231V1PositionData.cs index 42450b30..3eee2f4c 100644 --- a/OpenEphys.Onix1/TS4231V1PositionData.cs +++ b/OpenEphys.Onix1/TS4231V1PositionData.cs @@ -81,7 +81,6 @@ public class TS4231V1PositionData : Source /// A sequence of objects. public unsafe override IObservable Generate() { - return DeviceManager.GetDevice(DeviceName).SelectMany( deviceInfo => Observable.Create(observer => { @@ -103,8 +102,7 @@ public unsafe override IObservable Generate() return deviceInfo.Context .GetDeviceFrames(device.Address) .SubscribeSafe(frameObserver); - })) - .Select(input => new TS4231V1PositionDataFrame(input.Clock, input.HubClock, input.SensorIndex, input.Position)); + })); } } } From f6523343c87bd9a7a06af32974ecd8cde0f6cac4 Mon Sep 17 00:00:00 2001 From: cjsha Date: Thu, 12 Jun 2025 10:49:58 -0400 Subject: [PATCH 10/17] Remove System.Numerics from TS4231V1PositionData --- OpenEphys.Onix1/TS4231V1PositionData.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/OpenEphys.Onix1/TS4231V1PositionData.cs b/OpenEphys.Onix1/TS4231V1PositionData.cs index 3eee2f4c..d9038d8b 100644 --- a/OpenEphys.Onix1/TS4231V1PositionData.cs +++ b/OpenEphys.Onix1/TS4231V1PositionData.cs @@ -1,7 +1,6 @@ using System; using System.ComponentModel; using System.Linq; -using System.Numerics; using System.Reactive; using System.Reactive.Linq; using Bonsai; From f1f87ad9e211997be05c5aeab9be87bb63e47895 Mon Sep 17 00:00:00 2001 From: cjsha Date: Thu, 12 Jun 2025 14:35:15 -0400 Subject: [PATCH 11/17] Change SpatialTransformProperties struct to class --- .../SpatialTransformMatrixDialog.cs | 65 +++++++++---------- .../SpatialTransformMatrixEditor.cs | 2 +- OpenEphys.Onix1/SpatialTransformProperties.cs | 58 +++++++++++++++++ OpenEphys.Onix1/TS4231V1SpatialTransform.cs | 49 -------------- 4 files changed, 89 insertions(+), 85 deletions(-) create mode 100644 OpenEphys.Onix1/SpatialTransformProperties.cs diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs index 61943691..c5530bfd 100644 --- a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs +++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs @@ -17,26 +17,21 @@ public partial class SpatialTransformMatrixDialog : Form internal SpatialTransformProperties SpatialTransform; const byte NumMeasurements = 100; readonly IObservable PositionDataSource; - readonly Vector3[] UserCoordinates = { default, default, default, default }; - readonly Vector3[] TS4231Coordinates = { default, default, default, default }; - Matrix4x4? M; IDisposable richTextBoxStatusUpdateSubscription; IDisposable MeasurementCalculationSubscription; internal SpatialTransformMatrixDialog(IObservable dataSource, SpatialTransformProperties transformProperties) { InitializeComponent(); + SpatialTransform = transformProperties; PositionDataSource = dataSource; - M = transformProperties.M.GetValueOrDefault(); - Array.Copy(transformProperties.Pre, TS4231Coordinates, 4); var ts4231TextBoxes = new TextBox[] { textBoxTS4231Coordinate0, textBoxTS4231Coordinate1, textBoxTS4231Coordinate2, textBoxTS4231Coordinate3}; - foreach (var (textBox, v) in Enumerable.Zip(ts4231TextBoxes, TS4231Coordinates, (tb, v) => (tb, v))) + foreach (var (textBox, v) in Enumerable.Zip(ts4231TextBoxes, SpatialTransform.Pre, (tb, v) => (tb, v))) textBox.Text = checkVector3ForNaN(v) ? "" : $"{v.X}, {v.Y}, {v.Z}"; - Array.Copy(transformProperties.Post, UserCoordinates, 4); var userTextBoxes = new TextBox[] { textBoxUserCoordinate0X, textBoxUserCoordinate0Y, textBoxUserCoordinate0Z, textBoxUserCoordinate1X, textBoxUserCoordinate1Y, textBoxUserCoordinate1Z, @@ -44,7 +39,7 @@ internal SpatialTransformMatrixDialog(IObservable dat textBoxUserCoordinate3X, textBoxUserCoordinate3Y, textBoxUserCoordinate3Z}; for (byte i = 0; i < 12; i++) { - ref var component = ref GetComponent(ref UserCoordinates[i / 3], i % 3); + ref var component = ref GetComponent(ref SpatialTransform.Post[i / 3], i % 3); userTextBoxes[i].Text = float.IsNaN(component) ? "" : component.ToString(); } @@ -54,10 +49,9 @@ internal SpatialTransformMatrixDialog(IObservable dat private void TextBoxUserCoordinate_TextChanged(object sender, EventArgs e) { var tag = Convert.ToByte(((TextBox)sender).Tag); - ref var coordinateComponent = ref GetComponent(ref UserCoordinates[tag / 3], tag % 3); + ref var coordinateComponent = ref GetComponent(ref SpatialTransform.Post[tag / 3], tag % 3); try { coordinateComponent = float.Parse(((TextBox)sender).Text); } catch { coordinateComponent = float.NaN; } - M = null; CalculatePrintMatrix(); } @@ -66,12 +60,12 @@ private void ButtonMeasure_Click(object sender, EventArgs e) TextBox[] ts4231TextBoxes = { textBoxTS4231Coordinate0, textBoxTS4231Coordinate1, textBoxTS4231Coordinate2, textBoxTS4231Coordinate3 }; var index = Convert.ToByte(((Button)sender).Tag); ts4231TextBoxes[index].Text = ""; - TS4231Coordinates[index] = new(float.NaN); + SpatialTransform.Pre[index] = new(float.NaN); if (((Button)sender).Text == "Measure") { richTextBoxStatus.SelectionColor = Color.Blue; richTextBoxStatus.AppendText($"Measurement at coordinate {index} initiated.\n"); - M = null; + SpatialTransform.M = null; textBoxSpatialTransformMatrix.Text = ""; ((Button)sender).Text = "Cancel"; EnableButtons(false, index); @@ -115,8 +109,8 @@ private void ButtonMeasure_Click(object sender, EventArgs e) (acc, current) => (acc.Sum + current.Position, acc.Count + 1), acc => { - TS4231Coordinates[index] = acc.Sum / NumMeasurements; - return (Position: TS4231Coordinates[index], Valid: acc.Count == NumMeasurements); + SpatialTransform.Pre[index] = acc.Sum / NumMeasurements; + return (Position: SpatialTransform.Pre[index], Valid: acc.Count == NumMeasurements); }) .ObserveOn(new ControlScheduler(this)) .Subscribe(measurement => @@ -144,12 +138,13 @@ private void ButtonMeasure_Click(object sender, EventArgs e) private void ButtonOK_Click(object sender, EventArgs e) { - SpatialTransform = new SpatialTransformProperties(TS4231Coordinates, UserCoordinates, M.GetValueOrDefault()); - if (M == null) + if (SpatialTransform.M.HasValue) + DialogResult = DialogResult.OK; + else { var confirmationMessage = ""; var incompleteInput = false; - if (UserCoordinates.Any(userCoordinate => checkVector3ForNaN(userCoordinate))) + if (SpatialTransform.Post.Any(userCoordinate => checkVector3ForNaN(userCoordinate))) { incompleteInput = true; var axes = new char[] { 'X', 'Y', 'Z' }; @@ -157,36 +152,32 @@ private void ButtonOK_Click(object sender, EventArgs e) confirmationMessage += "At least one coordinate component is empty or invalid:\n"; for (byte i = 0; i < 12; i++) { - ref var component = ref GetComponent(ref UserCoordinates[i / 3], i % 3); + ref var component = ref GetComponent(ref SpatialTransform.Post[i / 3], i % 3); if (float.IsNaN(component)) confirmationMessage += $" • Coordinate {coordinates[i / 3]} {axes[i % 3]} component\n"; } confirmationMessage += "\n"; } - if (TS4231Coordinates.Any(TS4231Coordinate => checkVector3ForNaN(TS4231Coordinate))) + if (SpatialTransform.Pre.Any(TS4231Coordinate => checkVector3ForNaN(TS4231Coordinate))) { incompleteInput = true; confirmationMessage += "At least one coordinate measurement is empty:\n"; - foreach (var (i, v) in TS4231Coordinates.Select((i, v) => (v, i))) + foreach (var (i, v) in SpatialTransform.Pre.Select((i, v) => (v, i))) if (checkVector3ForNaN(v)) confirmationMessage += $" • Coordinate {i}\n"; confirmationMessage += "\n"; } if (incompleteInput) - confirmationMessage += "They will not be saved and position data won't properly output.\n\n"; - else if (!Matrix4x4.Invert(Vector3sToMatrix4x4(UserCoordinates), out _)) - confirmationMessage = "The spatial transform matrix is non-invertible " + - "(i.e. not all three axes are spanned in your coordinate selection or some coordinates are repeated). " + - "Position information will be incorrect.\n\n"; + confirmationMessage += "They will not be saved and transformed position data won't be properly output.\n\n"; + else if (!Matrix4x4.Invert(Vector3sToMatrix4x4(SpatialTransform.Post), out _)) + confirmationMessage = "The spatial transform matrix is non-invertible. The transformed position data won't be properly output.\n\n"; confirmationMessage += "Would you like to continue?"; if (MessageBox.Show(confirmationMessage, "Confirmation", MessageBoxButtons.YesNo) == DialogResult.Yes) DialogResult = DialogResult.OK; - } - else - DialogResult = DialogResult.OK; + } } private readonly Func checkVector3ForNaN = v => new[] { v.X, v.Y, v.Z }.Any(float.IsNaN); @@ -199,16 +190,16 @@ private void EnableButtons(bool enable, byte index) private void CalculatePrintMatrix() { - if (!UserCoordinates.Any(userCoordinate => checkVector3ForNaN(userCoordinate)) && - !TS4231Coordinates.Any(TS4231Coordinate => checkVector3ForNaN(TS4231Coordinate))) + SpatialTransform.M = null; + if (!SpatialTransform.Post.Any(userCoordinate => checkVector3ForNaN(userCoordinate)) && + !SpatialTransform.Pre.Any(TS4231Coordinate => checkVector3ForNaN(TS4231Coordinate))) { - if (Matrix4x4.Invert(Vector3sToMatrix4x4(UserCoordinates), out _)) + if (Matrix4x4.Invert(Vector3sToMatrix4x4(SpatialTransform.Post), out _)) { - var ts4231V1CoordinatesMatrix = Vector3sToMatrix4x4(TS4231Coordinates); - var userCoordinatesMatrix = Vector3sToMatrix4x4(UserCoordinates); + var ts4231V1CoordinatesMatrix = Vector3sToMatrix4x4(SpatialTransform.Pre); + var userCoordinatesMatrix = Vector3sToMatrix4x4(SpatialTransform.Post); Matrix4x4.Invert(ts4231V1CoordinatesMatrix, out var ts4231V1CoordinatesMatrixInverted); - M = Matrix4x4.Multiply(ts4231V1CoordinatesMatrixInverted, userCoordinatesMatrix); - textBoxSpatialTransformMatrix.Text = M.Value.ToString(); + SpatialTransform.M = Matrix4x4.Multiply(ts4231V1CoordinatesMatrixInverted, userCoordinatesMatrix); toolStripStatusLabel.Image = Properties.Resources.StatusReadyImage; toolStripStatusLabel.Text = "Spatial transform matrix is calculated."; } @@ -223,6 +214,10 @@ private void CalculatePrintMatrix() toolStripStatusLabel.Image = Properties.Resources.StatusWarningImage; toolStripStatusLabel.Text = "All fields must be properly populated."; } + if (SpatialTransform.M.HasValue) + textBoxSpatialTransformMatrix.Text = SpatialTransform.M.Value.ToString(); + else + textBoxSpatialTransformMatrix.Text = ""; } private static ref float GetComponent(ref Vector3 v, int index) diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs b/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs index bce7883a..66f8bca4 100644 --- a/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs +++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs @@ -33,7 +33,7 @@ public override object EditValue(ITypeDescriptorContext context, IServiceProvide { var source = GetDataSource(context, provider); var dataFrames = source.Output.Merge().Select(x => x as TS4231V1PositionDataFrame); - using var visualizerDialog = new SpatialTransformMatrixDialog(dataFrames, (SpatialTransformProperties)value); + using var visualizerDialog = new SpatialTransformMatrixDialog(dataFrames, new SpatialTransformProperties((SpatialTransformProperties)value)); if (!editorState.WorkflowRunning) { throw new InvalidOperationException("Workflow must be running to open this GUI."); diff --git a/OpenEphys.Onix1/SpatialTransformProperties.cs b/OpenEphys.Onix1/SpatialTransformProperties.cs new file mode 100644 index 00000000..f20306b5 --- /dev/null +++ b/OpenEphys.Onix1/SpatialTransformProperties.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text; +using System.Threading.Tasks; + +namespace OpenEphys.Onix1 +{ + /// + /// Data necessary to construct a spatial transform matrix as well as the + /// spatial transform matrix itself. + /// + public class SpatialTransformProperties + { + /// + /// The set of coordinates before undergoing a spatial transform. + /// + public Vector3[] Pre { get; set; } + + /// + /// The set of coordinates after undergoing a spatial transform. + /// + public Vector3[] Post { get; set; } + + /// + /// The spatial transform matrix calculated from and + /// . + /// + public Matrix4x4? M { get; set; } + + /// + /// Initializes a new instance of the class with default values. + /// + public SpatialTransformProperties() + { + Pre = new Vector3[] { new(float.NaN), new(float.NaN), new(float.NaN), new(float.NaN) }; + Post = new Vector3[] { new(float.NaN), new(float.NaN), new(float.NaN), new(float.NaN) }; + M = null; + } + + /// + /// Initializes a new instance of the class as a copy of an existing + /// instance. + /// + /// The instance to copy. + public SpatialTransformProperties(SpatialTransformProperties other) + { + Pre = new Vector3[4]; + Post = new Vector3[4]; + Array.Copy(other.Pre, Pre, 4); + Array.Copy(other.Post, Post, 4); + M = other.M; + } + } +} diff --git a/OpenEphys.Onix1/TS4231V1SpatialTransform.cs b/OpenEphys.Onix1/TS4231V1SpatialTransform.cs index 520f4c30..7176f6d1 100644 --- a/OpenEphys.Onix1/TS4231V1SpatialTransform.cs +++ b/OpenEphys.Onix1/TS4231V1SpatialTransform.cs @@ -37,53 +37,4 @@ public override IObservable Process(IObservable - /// Data necessary to construct a spatial transform matrix as well as the - /// spatial transform matrix itself. - /// - public readonly record struct SpatialTransformProperties - { - /// - /// A set of coodinates before undergoing a spatial transform. - /// - public readonly Vector3[] Pre = { new(float.NaN), new(float.NaN), new(float.NaN), new(float.NaN) }; - /// - /// A set of coodinates after undergoing a spatial transform. - /// - public readonly Vector3[] Post = { new(float.NaN), new(float.NaN), new(float.NaN), new(float.NaN) }; - /// - /// The spatial transform matrix calculated from and . - /// - public readonly Matrix4x4? M = null; - - /// - /// Initializes a new instance of the struct with default values. - /// - public SpatialTransformProperties() { } - - /// - /// Initializes a new instance of the with values specified using - /// parameters. - /// - /// - /// The value used to set . - /// - /// - /// The value used to set . - /// - /// - /// The value used to set . - /// - public SpatialTransformProperties(Vector3[] pre, Vector3[] post, Matrix4x4 m) - { - Array.Copy(pre, Pre, 4); - Array.Copy(post, Post, 4); - M = m; - } - } } From 5b1074b95fcf627fa56ab17fe5e882f94dc3c6dc Mon Sep 17 00:00:00 2001 From: cjsha Date: Fri, 1 Aug 2025 16:53:21 -0400 Subject: [PATCH 12/17] Refactor SpatialTransform3D per jonnew's PR feedback --- .../SpatialTransformMatrixDialog.cs | 182 ++++++++---------- .../SpatialTransformMatrixEditor.cs | 2 +- OpenEphys.Onix1/SpatialTransform3D.cs | 120 ++++++++++++ OpenEphys.Onix1/SpatialTransformProperties.cs | 58 ------ OpenEphys.Onix1/TS4231V1SpatialTransform.cs | 4 +- .../TS4231V1TransformedPositionData.bonsai | 74 ++++--- 6 files changed, 247 insertions(+), 193 deletions(-) create mode 100644 OpenEphys.Onix1/SpatialTransform3D.cs delete mode 100644 OpenEphys.Onix1/SpatialTransformProperties.cs diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs index c5530bfd..32c2c577 100644 --- a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs +++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs @@ -1,11 +1,10 @@ using System; +using System.Drawing; using System.Linq; using System.Numerics; -using System.Windows.Forms; using System.Reactive.Linq; +using System.Windows.Forms; using Bonsai.Design; -using System.Collections.Generic; -using System.Drawing; namespace OpenEphys.Onix1.Design { @@ -14,58 +13,61 @@ namespace OpenEphys.Onix1.Design /// public partial class SpatialTransformMatrixDialog : Form { - internal SpatialTransformProperties SpatialTransform; + internal SpatialTransform3D SpatialTransform; const byte NumMeasurements = 100; readonly IObservable PositionDataSource; IDisposable richTextBoxStatusUpdateSubscription; IDisposable MeasurementCalculationSubscription; - internal SpatialTransformMatrixDialog(IObservable dataSource, SpatialTransformProperties transformProperties) + internal SpatialTransformMatrixDialog(IObservable dataSource, SpatialTransform3D transformProperties) { InitializeComponent(); SpatialTransform = transformProperties; PositionDataSource = dataSource; - var ts4231TextBoxes = new TextBox[] { - textBoxTS4231Coordinate0, textBoxTS4231Coordinate1, - textBoxTS4231Coordinate2, textBoxTS4231Coordinate3}; - foreach (var (textBox, v) in Enumerable.Zip(ts4231TextBoxes, SpatialTransform.Pre, (tb, v) => (tb, v))) - textBox.Text = checkVector3ForNaN(v) ? "" : $"{v.X}, {v.Y}, {v.Z}"; + var ts4231TextBoxes = new TextBox[] { + textBoxTS4231Coordinate0, textBoxTS4231Coordinate1, + textBoxTS4231Coordinate2, textBoxTS4231Coordinate3 }; + var preTransformCoordinates = SpatialTransform.MatrixToFloatArray(SpatialTransform.A); + for (byte i = 0; i < 4; i++) + ts4231TextBoxes[i].Text = float.IsNaN(preTransformCoordinates[i * 3]) ? "" : $"{preTransformCoordinates[i * 3]}, " + + $"{preTransformCoordinates[i * 3 + 1]}, " + + $"{preTransformCoordinates[i * 3 + 2]}"; var userTextBoxes = new TextBox[] { textBoxUserCoordinate0X, textBoxUserCoordinate0Y, textBoxUserCoordinate0Z, textBoxUserCoordinate1X, textBoxUserCoordinate1Y, textBoxUserCoordinate1Z, textBoxUserCoordinate2X, textBoxUserCoordinate2Y, textBoxUserCoordinate2Z, - textBoxUserCoordinate3X, textBoxUserCoordinate3Y, textBoxUserCoordinate3Z}; - for (byte i = 0; i < 12; i++) - { - ref var component = ref GetComponent(ref SpatialTransform.Post[i / 3], i % 3); - userTextBoxes[i].Text = float.IsNaN(component) ? "" : component.ToString(); - } + textBoxUserCoordinate3X, textBoxUserCoordinate3Y, textBoxUserCoordinate3Z }; + var postTransformCoordinates = SpatialTransform.MatrixToFloatArray(SpatialTransform.B); + foreach (var (tb, comp) in Enumerable.Zip(userTextBoxes, postTransformCoordinates, (tb, comp) => (tb, comp))) + tb.Text = float.IsNaN(comp) ? "" : comp.ToString(); - CalculatePrintMatrix(); + IndicateSpatialTransformStatus(); } private void TextBoxUserCoordinate_TextChanged(object sender, EventArgs e) { var tag = Convert.ToByte(((TextBox)sender).Tag); - ref var coordinateComponent = ref GetComponent(ref SpatialTransform.Post[tag / 3], tag % 3); - try { coordinateComponent = float.Parse(((TextBox)sender).Text); } - catch { coordinateComponent = float.NaN; } - CalculatePrintMatrix(); + try { SpatialTransform.SetMatrixBElement(float.Parse(((TextBox)sender).Text), tag / 3, tag % 3); } + catch { SpatialTransform.SetMatrixBElement(float.NaN, tag / 3, tag % 3); } + IndicateSpatialTransformStatus(); } private void ButtonMeasure_Click(object sender, EventArgs e) { TextBox[] ts4231TextBoxes = { textBoxTS4231Coordinate0, textBoxTS4231Coordinate1, textBoxTS4231Coordinate2, textBoxTS4231Coordinate3 }; var index = Convert.ToByte(((Button)sender).Tag); + + for (byte i = 0; i < 3; i++) + SpatialTransform.SetMatrixAElement(float.NaN, index, i); ts4231TextBoxes[index].Text = ""; - SpatialTransform.Pre[index] = new(float.NaN); + if (((Button)sender).Text == "Measure") { richTextBoxStatus.SelectionColor = Color.Blue; richTextBoxStatus.AppendText($"Measurement at coordinate {index} initiated.\n"); - SpatialTransform.M = null; + IndicateSpatialTransformStatus(); textBoxSpatialTransformMatrix.Text = ""; ((Button)sender).Text = "Cancel"; EnableButtons(false, index); @@ -109,8 +111,11 @@ private void ButtonMeasure_Click(object sender, EventArgs e) (acc, current) => (acc.Sum + current.Position, acc.Count + 1), acc => { - SpatialTransform.Pre[index] = acc.Sum / NumMeasurements; - return (Position: SpatialTransform.Pre[index], Valid: acc.Count == NumMeasurements); + var measurement = acc.Sum / NumMeasurements; + SpatialTransform.SetMatrixAElement(measurement.X, index, 0); + SpatialTransform.SetMatrixAElement(measurement.Y, index, 1); + SpatialTransform.SetMatrixAElement(measurement.Z, index, 2); + return (Position: measurement, Valid: acc.Count == NumMeasurements); }) .ObserveOn(new ControlScheduler(this)) .Subscribe(measurement => @@ -119,7 +124,7 @@ private void ButtonMeasure_Click(object sender, EventArgs e) if (measurement.Valid) { ts4231TextBoxes[index].Text = $"{measurement.Position.X}, {measurement.Position.Y}, {measurement.Position.Z}"; - CalculatePrintMatrix(); + IndicateSpatialTransformStatus(); } }); @@ -138,106 +143,69 @@ private void ButtonMeasure_Click(object sender, EventArgs e) private void ButtonOK_Click(object sender, EventArgs e) { - if (SpatialTransform.M.HasValue) - DialogResult = DialogResult.OK; - else + var confirmationMessage = ""; + var invalidInput = false; + if (SpatialTransform.ContainsNaN(SpatialTransform.A) || SpatialTransform.ContainsNaN(SpatialTransform.B)) { - var confirmationMessage = ""; - var incompleteInput = false; - if (SpatialTransform.Post.Any(userCoordinate => checkVector3ForNaN(userCoordinate))) - { - incompleteInput = true; - var axes = new char[] { 'X', 'Y', 'Z' }; - var coordinates = new byte[] { 0, 1, 2, 3 }; - confirmationMessage += "At least one coordinate component is empty or invalid:\n"; - for (byte i = 0; i < 12; i++) - { - ref var component = ref GetComponent(ref SpatialTransform.Post[i / 3], i % 3); - if (float.IsNaN(component)) - confirmationMessage += $" • Coordinate {coordinates[i / 3]} {axes[i % 3]} component\n"; - } - confirmationMessage += "\n"; - } - if (SpatialTransform.Pre.Any(TS4231Coordinate => checkVector3ForNaN(TS4231Coordinate))) - { - incompleteInput = true; - confirmationMessage += "At least one coordinate measurement is empty:\n"; - foreach (var (i, v) in SpatialTransform.Pre.Select((i, v) => (v, i))) - if (checkVector3ForNaN(v)) - confirmationMessage += $" • Coordinate {i}\n"; - confirmationMessage += "\n"; - } - - if (incompleteInput) - confirmationMessage += "They will not be saved and transformed position data won't be properly output.\n\n"; - else if (!Matrix4x4.Invert(Vector3sToMatrix4x4(SpatialTransform.Post), out _)) - confirmationMessage = "The spatial transform matrix is non-invertible. The transformed position data won't be properly output.\n\n"; - - confirmationMessage += "Would you like to continue?"; + confirmationMessage = $"At least one entry in the {Name} is invalid for calculating a proper 3D spatial transform:\n"; + + var axes = new char[] { 'X', 'Y', 'Z' }; + var coordinates = new byte[] { 0, 1, 2, 3 }; + + for (byte i = 0; i < 12; i++) + if (float.IsNaN(SpatialTransform.MatrixToFloatArray(SpatialTransform.B)[i])) + confirmationMessage += $" • Component {axes[i % 3]} from user coordinate {coordinates[i / 3]}\n"; + for (byte i = 0; i < 4; i++) + if (float.IsNaN(SpatialTransform.MatrixToFloatArray(SpatialTransform.A)[i * 3])) + confirmationMessage += $" • TS4231 Coordinate {i}\n"; + + confirmationMessage += "\nThese invalid entries will not be saved. "; + invalidInput = true; + } + else if (!Matrix4x4.Invert(SpatialTransform.M, out _)) + { + confirmationMessage = $"The calculated spatial transform matrix is non-invertible. "; + invalidInput = true; + } + + if (invalidInput) + { + confirmationMessage += "The transformed position data will be NaNs until these entries are fixed.\n\n" + + "Would you like to continue?"; if (MessageBox.Show(confirmationMessage, "Confirmation", MessageBoxButtons.YesNo) == DialogResult.Yes) DialogResult = DialogResult.OK; - } + } + else + DialogResult = DialogResult.OK; } - private readonly Func checkVector3ForNaN = v => new[] { v.X, v.Y, v.Z }.Any(float.IsNaN); - private void EnableButtons(bool enable, byte index) { var buttons = new Button[] { buttonMeasure0, buttonMeasure1, buttonMeasure2, buttonMeasure3, buttonOK, buttonCancel }; Array.ForEach(buttons, button => button.Enabled = enable || (Convert.ToByte(button.Tag) == index)); } - private void CalculatePrintMatrix() + private void IndicateSpatialTransformStatus() { - SpatialTransform.M = null; - if (!SpatialTransform.Post.Any(userCoordinate => checkVector3ForNaN(userCoordinate)) && - !SpatialTransform.Pre.Any(TS4231Coordinate => checkVector3ForNaN(TS4231Coordinate))) - { - if (Matrix4x4.Invert(Vector3sToMatrix4x4(SpatialTransform.Post), out _)) - { - var ts4231V1CoordinatesMatrix = Vector3sToMatrix4x4(SpatialTransform.Pre); - var userCoordinatesMatrix = Vector3sToMatrix4x4(SpatialTransform.Post); - Matrix4x4.Invert(ts4231V1CoordinatesMatrix, out var ts4231V1CoordinatesMatrixInverted); - SpatialTransform.M = Matrix4x4.Multiply(ts4231V1CoordinatesMatrixInverted, userCoordinatesMatrix); - toolStripStatusLabel.Image = Properties.Resources.StatusReadyImage; - toolStripStatusLabel.Text = "Spatial transform matrix is calculated."; - } - else - { - toolStripStatusLabel.Image = Properties.Resources.StatusWarningImage; - toolStripStatusLabel.Text = "The resulting spatial transform matrix must be non-invertible."; - } - } - else + if (SpatialTransform.ContainsNaN(SpatialTransform.A) || SpatialTransform.ContainsNaN(SpatialTransform.B)) { toolStripStatusLabel.Image = Properties.Resources.StatusWarningImage; toolStripStatusLabel.Text = "All fields must be properly populated."; + textBoxSpatialTransformMatrix.Text = ""; } - if (SpatialTransform.M.HasValue) - textBoxSpatialTransformMatrix.Text = SpatialTransform.M.Value.ToString(); - else + else if (!Matrix4x4.Invert(SpatialTransform.M, out _)) + { + toolStripStatusLabel.Image = Properties.Resources.StatusWarningImage; + toolStripStatusLabel.Text = "The calculated spatial transform matrix must be invertible."; textBoxSpatialTransformMatrix.Text = ""; - } - - private static ref float GetComponent(ref Vector3 v, int index) - { - switch (index) + } + else { - case 0: return ref v.X; - case 1: return ref v.Y; - case 2: return ref v.Z; - default: throw new IndexOutOfRangeException(); - }; - } - - private Matrix4x4 Vector3sToMatrix4x4(IList rows) - { - return new Matrix4x4( - rows[0].X, rows[0].Y, rows[0].Z, 1, - rows[1].X, rows[1].Y, rows[1].Z, 1, - rows[2].X, rows[2].Y, rows[2].Z, 1, - rows[3].X, rows[3].Y, rows[3].Z, 1); + toolStripStatusLabel.Image = Properties.Resources.StatusReadyImage; + toolStripStatusLabel.Text = "Spatial transform matrix is calculated."; + textBoxSpatialTransformMatrix.Text = SpatialTransform.M.ToString(); + } } } } diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs b/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs index 66f8bca4..6be7ac3e 100644 --- a/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs +++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs @@ -33,7 +33,7 @@ public override object EditValue(ITypeDescriptorContext context, IServiceProvide { var source = GetDataSource(context, provider); var dataFrames = source.Output.Merge().Select(x => x as TS4231V1PositionDataFrame); - using var visualizerDialog = new SpatialTransformMatrixDialog(dataFrames, new SpatialTransformProperties((SpatialTransformProperties)value)); + using var visualizerDialog = new SpatialTransformMatrixDialog(dataFrames, new SpatialTransform3D((SpatialTransform3D)value)); if (!editorState.WorkflowRunning) { throw new InvalidOperationException("Workflow must be running to open this GUI."); diff --git a/OpenEphys.Onix1/SpatialTransform3D.cs b/OpenEphys.Onix1/SpatialTransform3D.cs new file mode 100644 index 00000000..15dd1959 --- /dev/null +++ b/OpenEphys.Onix1/SpatialTransform3D.cs @@ -0,0 +1,120 @@ +using System; +using System.Linq; +using System.Numerics; +using System.Xml.Serialization; + +namespace OpenEphys.Onix1 +{ + /// + /// Data necessary to construct a spatial transform matrix as well as the + /// spatial transform matrix itself. + /// + public class SpatialTransform3D + { + + private Matrix4x4 _a, _b; + + /// + /// The A matrix in A * = . It is + /// constructed from a set of four Cartesian coordinates before + /// undergoing a spatial transformation. + /// + public Matrix4x4 A { get => _a; set { _a = value; UpdateM(); } } + + /// + /// The B matrix in * = B. It is + /// constructed from a set of four Cartesian coordinates after + /// undergoing a spatial transformation. + /// + public Matrix4x4 B { get => _b ; set { _b = value; UpdateM(); } } + + /// + /// The M matrix in * = M. It is the + /// spatial transform matrix. It calculated as M = A.inv * B. + /// + [XmlIgnore] + public Matrix4x4 M { get; private set; } + + /// + /// Initializes a new instance of the + /// class with default values. + /// + public SpatialTransform3D() + { + A = B = new(float.NaN, float.NaN, float.NaN, 1, + float.NaN, float.NaN, float.NaN, 1, + float.NaN, float.NaN, float.NaN, 1, + float.NaN, float.NaN, float.NaN, 1); + M = new(float.NaN, float.NaN, float.NaN, float.NaN, + float.NaN, float.NaN, float.NaN, float.NaN, + float.NaN, float.NaN, float.NaN, float.NaN, + float.NaN, float.NaN, float.NaN, float.NaN); + } + + /// + /// Initializes a new instance of the + /// class as a copy of an existing instance. + /// + /// The instance to copy. + public SpatialTransform3D(SpatialTransform3D other) + { + A = other.A; + B = other.B; + } + + /// + /// Sets a component (X, Y, or Z) in one of the coordinates in + /// PreTransformCoordinates. + /// + public void SetMatrixAElement(float value, int coordinate, int component) => + SetMatrixElement(ref _a, value, coordinate, component); + + /// + /// Sets a component (X, Y, or Z) in one of the coordinates in + /// PostTransformCoordinates. + /// + public void SetMatrixBElement(float value, int coordinate, int component) => + SetMatrixElement(ref _b, value, coordinate, component); + + private void SetMatrixElement(ref Matrix4x4 m, float value, int coordinate, int component) + { + if (coordinate is < 0 or > 3) throw new ArgumentOutOfRangeException(nameof(coordinate) + " must be 0, 1, 2, or 3."); + if (component is < 0 or > 2) throw new ArgumentOutOfRangeException(nameof(component) + " must be 0, 1, or 2."); + + switch ((coordinate, component)) + { + case (0, 0): m.M11 = value; break; case (0, 1): m.M12 = value; break; case (0, 2): m.M13 = value; break; + case (1, 0): m.M21 = value; break; case (1, 1): m.M22 = value; break; case (1, 2): m.M23 = value; break; + case (2, 0): m.M31 = value; break; case (2, 1): m.M32 = value; break; case (2, 2): m.M33 = value; break; + case (3, 0): m.M41 = value; break; case (3, 1): m.M42 = value; break; case (3, 2): m.M43 = value; break; + } + UpdateM(); + } + + private void UpdateM() + { + + Matrix4x4.Invert(A, out var AInverted); + var m = Matrix4x4.Multiply(AInverted, B); + M = !ContainsNaN(m) && Matrix4x4.Invert(m, out _) ? m : + new(float.NaN, float.NaN, float.NaN, float.NaN, + float.NaN, float.NaN, float.NaN, float.NaN, + float.NaN, float.NaN, float.NaN, float.NaN, + float.NaN, float.NaN, float.NaN, float.NaN); + } + + /// + /// Convert coordinates from matrix to a float array. + /// + public float[] MatrixToFloatArray(Matrix4x4 m) => + new float[] { m.M11, m.M12, m.M13, + m.M21, m.M22, m.M23, + m.M31, m.M32, m.M33, + m.M41, m.M42, m.M43 }; + + /// + /// Checks if matrix contains one or more NaNs. + /// + public bool ContainsNaN(Matrix4x4 m) => MatrixToFloatArray(m).Any(float.IsNaN); + } +} diff --git a/OpenEphys.Onix1/SpatialTransformProperties.cs b/OpenEphys.Onix1/SpatialTransformProperties.cs deleted file mode 100644 index f20306b5..00000000 --- a/OpenEphys.Onix1/SpatialTransformProperties.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using System.Text; -using System.Threading.Tasks; - -namespace OpenEphys.Onix1 -{ - /// - /// Data necessary to construct a spatial transform matrix as well as the - /// spatial transform matrix itself. - /// - public class SpatialTransformProperties - { - /// - /// The set of coordinates before undergoing a spatial transform. - /// - public Vector3[] Pre { get; set; } - - /// - /// The set of coordinates after undergoing a spatial transform. - /// - public Vector3[] Post { get; set; } - - /// - /// The spatial transform matrix calculated from and - /// . - /// - public Matrix4x4? M { get; set; } - - /// - /// Initializes a new instance of the class with default values. - /// - public SpatialTransformProperties() - { - Pre = new Vector3[] { new(float.NaN), new(float.NaN), new(float.NaN), new(float.NaN) }; - Post = new Vector3[] { new(float.NaN), new(float.NaN), new(float.NaN), new(float.NaN) }; - M = null; - } - - /// - /// Initializes a new instance of the class as a copy of an existing - /// instance. - /// - /// The instance to copy. - public SpatialTransformProperties(SpatialTransformProperties other) - { - Pre = new Vector3[4]; - Post = new Vector3[4]; - Array.Copy(other.Pre, Pre, 4); - Array.Copy(other.Post, Post, 4); - M = other.M; - } - } -} diff --git a/OpenEphys.Onix1/TS4231V1SpatialTransform.cs b/OpenEphys.Onix1/TS4231V1SpatialTransform.cs index 7176f6d1..74751300 100644 --- a/OpenEphys.Onix1/TS4231V1SpatialTransform.cs +++ b/OpenEphys.Onix1/TS4231V1SpatialTransform.cs @@ -19,7 +19,7 @@ public class TS4231V1SpatialTransform : Transform [Editor("OpenEphys.Onix1.Design.SpatialTransformMatrixEditor, OpenEphys.Onix1.Design", DesignTypes.UITypeEditor)] [Description("Data for transforming position measurements to another reference frame.")] - public SpatialTransformProperties SpatialTransform { get; set; } = new(); + public SpatialTransform3D SpatialTransform { get; set; } = new(); /// /// Transforms a sequence of @@ -34,7 +34,7 @@ public override IObservable Process(IObservable new TS4231V1PositionDataFrame(input.Clock, input.HubClock, input.SensorIndex, - Vector3.Transform(input.Position, SpatialTransform.M.GetValueOrDefault()))); + Vector3.Transform(input.Position, SpatialTransform.M))); } } } diff --git a/OpenEphys.Onix1/TS4231V1TransformedPositionData.bonsai b/OpenEphys.Onix1/TS4231V1TransformedPositionData.bonsai index 9a8a7a35..60dd9e21 100644 --- a/OpenEphys.Onix1/TS4231V1TransformedPositionData.bonsai +++ b/OpenEphys.Onix1/TS4231V1TransformedPositionData.bonsai @@ -1,5 +1,5 @@  - @@ -20,29 +20,6 @@ 0 0 - - 1 - 0 - 0 - 0 - 0 - 1 - 0 - 0 - 0 - 0 - 1 - 0 - 0 - 0 - 0 - 1 - - 0 - 0 - 0 - - @@ -50,7 +27,54 @@ - + + + NaN + NaN + NaN + 1 + NaN + NaN + NaN + 1 + NaN + NaN + NaN + 1 + NaN + NaN + NaN + 1 + + NaN + NaN + NaN + + + + NaN + NaN + NaN + 1 + NaN + NaN + NaN + 1 + NaN + NaN + NaN + 1 + NaN + NaN + NaN + 1 + + NaN + NaN + NaN + + + From 08e7962eff1af6a20ee29b395fca0b1213184244 Mon Sep 17 00:00:00 2001 From: cjsha Date: Tue, 15 Apr 2025 16:33:24 -0400 Subject: [PATCH 13/17] Add operator to spatial transform ts4231 data --- .../SpatialTransformMatrixDialog.Designer.cs | 472 +++++ .../SpatialTransformMatrixDialog.cs | 140 ++ .../SpatialTransformMatrixDialog.resx | 1784 +++++++++++++++++ .../SpatialTransformMatrixEditor.cs | 54 + OpenEphys.Onix1/SpatialTransform.cs | 36 + 5 files changed, 2486 insertions(+) create mode 100644 OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.Designer.cs create mode 100644 OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs create mode 100644 OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.resx create mode 100644 OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs create mode 100644 OpenEphys.Onix1/SpatialTransform.cs diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.Designer.cs b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.Designer.cs new file mode 100644 index 00000000..eea32388 --- /dev/null +++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.Designer.cs @@ -0,0 +1,472 @@ +using Bonsai.Design; + +namespace OpenEphys.Onix1.Design +{ + partial class SpatialTransformMatrixDialog + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(SpatialTransformMatrixDialog)); + this.tableLayoutPanelMain = new System.Windows.Forms.TableLayoutPanel(); + this.groupBoxStatus = new System.Windows.Forms.GroupBox(); + this.textBoxStatus = new System.Windows.Forms.TextBox(); + this.labelInstructions = new System.Windows.Forms.Label(); + this.tableLayoutPanelCoordinates = new System.Windows.Forms.TableLayoutPanel(); + this.textBoxUserCoordinate3 = new System.Windows.Forms.TextBox(); + this.textBoxUserCoordinate2 = new System.Windows.Forms.TextBox(); + this.textBoxUserCoordinate1 = new System.Windows.Forms.TextBox(); + this.textBoxUserCoordinate0 = new System.Windows.Forms.TextBox(); + this.textBoxTS4231Coordinate3 = new System.Windows.Forms.TextBox(); + this.textBoxTS4231Coordinate2 = new System.Windows.Forms.TextBox(); + this.textBoxTS4231Coordinate1 = new System.Windows.Forms.TextBox(); + this.buttonMeasure3 = new System.Windows.Forms.Button(); + this.buttonMeasure2 = new System.Windows.Forms.Button(); + this.buttonMeasure1 = new System.Windows.Forms.Button(); + this.labelCoordinate3 = new System.Windows.Forms.Label(); + this.labelCoordinate2 = new System.Windows.Forms.Label(); + this.labelCoordinate1 = new System.Windows.Forms.Label(); + this.buttonMeasure0 = new System.Windows.Forms.Button(); + this.labelHeaderTS4231 = new System.Windows.Forms.Label(); + this.labelCoordinate0 = new System.Windows.Forms.Label(); + this.textBoxTS4231Coordinate0 = new System.Windows.Forms.TextBox(); + this.labelHeaderUser = new System.Windows.Forms.Label(); + this.buttonCalculate = new System.Windows.Forms.Button(); + this.flowLayoutPanelBottom = new System.Windows.Forms.FlowLayoutPanel(); + this.buttonClose = new System.Windows.Forms.Button(); + this.checkBoxApplySpatialTransform = new System.Windows.Forms.CheckBox(); + this.tableLayoutPanelMain.SuspendLayout(); + this.groupBoxStatus.SuspendLayout(); + this.tableLayoutPanelCoordinates.SuspendLayout(); + this.flowLayoutPanelBottom.SuspendLayout(); + this.SuspendLayout(); + // + // tableLayoutPanelMain + // + this.tableLayoutPanelMain.ColumnCount = 1; + this.tableLayoutPanelMain.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); + this.tableLayoutPanelMain.Controls.Add(this.groupBoxStatus, 0, 2); + this.tableLayoutPanelMain.Controls.Add(this.labelInstructions, 0, 0); + this.tableLayoutPanelMain.Controls.Add(this.tableLayoutPanelCoordinates, 0, 1); + this.tableLayoutPanelMain.Controls.Add(this.buttonCalculate, 0, 3); + this.tableLayoutPanelMain.Controls.Add(this.flowLayoutPanelBottom, 0, 4); + this.tableLayoutPanelMain.Dock = System.Windows.Forms.DockStyle.Fill; + this.tableLayoutPanelMain.Location = new System.Drawing.Point(0, 0); + this.tableLayoutPanelMain.Name = "tableLayoutPanelMain"; + this.tableLayoutPanelMain.RowCount = 5; + this.tableLayoutPanelMain.RowStyles.Add(new System.Windows.Forms.RowStyle()); + this.tableLayoutPanelMain.RowStyles.Add(new System.Windows.Forms.RowStyle()); + this.tableLayoutPanelMain.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F)); + this.tableLayoutPanelMain.RowStyles.Add(new System.Windows.Forms.RowStyle()); + this.tableLayoutPanelMain.RowStyles.Add(new System.Windows.Forms.RowStyle()); + this.tableLayoutPanelMain.Size = new System.Drawing.Size(624, 661); + this.tableLayoutPanelMain.TabIndex = 7; + // + // groupBoxStatus + // + this.groupBoxStatus.Controls.Add(this.textBoxStatus); + this.groupBoxStatus.Dock = System.Windows.Forms.DockStyle.Fill; + this.groupBoxStatus.Location = new System.Drawing.Point(3, 263); + this.groupBoxStatus.Name = "groupBoxStatus"; + this.groupBoxStatus.Size = new System.Drawing.Size(618, 330); + this.groupBoxStatus.TabIndex = 6; + this.groupBoxStatus.TabStop = false; + this.groupBoxStatus.Text = "Status Messages"; + // + // textBoxStatus + // + this.textBoxStatus.AcceptsReturn = true; + this.textBoxStatus.AcceptsTab = true; + this.textBoxStatus.Dock = System.Windows.Forms.DockStyle.Fill; + this.textBoxStatus.Location = new System.Drawing.Point(3, 16); + this.textBoxStatus.Multiline = true; + this.textBoxStatus.Name = "textBoxStatus"; + this.textBoxStatus.ReadOnly = true; + this.textBoxStatus.ScrollBars = System.Windows.Forms.ScrollBars.Vertical; + this.textBoxStatus.Size = new System.Drawing.Size(612, 311); + this.textBoxStatus.TabIndex = 3; + this.textBoxStatus.Text = "Awaiting user input...\r\n"; + // + // labelInstructions + // + this.labelInstructions.AutoSize = true; + this.labelInstructions.Dock = System.Windows.Forms.DockStyle.Fill; + this.labelInstructions.Location = new System.Drawing.Point(3, 0); + this.labelInstructions.MaximumSize = new System.Drawing.Size(620, 0); + this.labelInstructions.Name = "labelInstructions"; + this.labelInstructions.Size = new System.Drawing.Size(618, 104); + this.labelInstructions.TabIndex = 4; + this.labelInstructions.Text = resources.GetString("labelInstructions.Text"); + // + // tableLayoutPanelCoordinates + // + this.tableLayoutPanelCoordinates.ColumnCount = 4; + this.tableLayoutPanelCoordinates.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); + this.tableLayoutPanelCoordinates.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); + this.tableLayoutPanelCoordinates.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F)); + this.tableLayoutPanelCoordinates.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F)); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxUserCoordinate3, 3, 4); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxUserCoordinate2, 3, 3); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxUserCoordinate1, 3, 2); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxUserCoordinate0, 3, 1); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxTS4231Coordinate3, 2, 4); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxTS4231Coordinate2, 2, 3); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxTS4231Coordinate1, 2, 2); + this.tableLayoutPanelCoordinates.Controls.Add(this.buttonMeasure3, 1, 4); + this.tableLayoutPanelCoordinates.Controls.Add(this.buttonMeasure2, 1, 3); + this.tableLayoutPanelCoordinates.Controls.Add(this.buttonMeasure1, 1, 2); + this.tableLayoutPanelCoordinates.Controls.Add(this.labelCoordinate3, 0, 4); + this.tableLayoutPanelCoordinates.Controls.Add(this.labelCoordinate2, 0, 3); + this.tableLayoutPanelCoordinates.Controls.Add(this.labelCoordinate1, 0, 2); + this.tableLayoutPanelCoordinates.Controls.Add(this.buttonMeasure0, 1, 1); + this.tableLayoutPanelCoordinates.Controls.Add(this.labelHeaderTS4231, 1, 0); + this.tableLayoutPanelCoordinates.Controls.Add(this.labelCoordinate0, 0, 1); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxTS4231Coordinate0, 2, 1); + this.tableLayoutPanelCoordinates.Controls.Add(this.labelHeaderUser, 3, 0); + this.tableLayoutPanelCoordinates.Dock = System.Windows.Forms.DockStyle.Fill; + this.tableLayoutPanelCoordinates.Location = new System.Drawing.Point(3, 107); + this.tableLayoutPanelCoordinates.Name = "tableLayoutPanelCoordinates"; + this.tableLayoutPanelCoordinates.RowCount = 5; + this.tableLayoutPanelCoordinates.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 30F)); + this.tableLayoutPanelCoordinates.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 30F)); + this.tableLayoutPanelCoordinates.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 30F)); + this.tableLayoutPanelCoordinates.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 30F)); + this.tableLayoutPanelCoordinates.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 30F)); + this.tableLayoutPanelCoordinates.Size = new System.Drawing.Size(618, 150); + this.tableLayoutPanelCoordinates.TabIndex = 5; + this.tableLayoutPanelCoordinates.Tag = "6"; + // + // textBoxUserCoordinate3 + // + this.textBoxUserCoordinate3.Dock = System.Windows.Forms.DockStyle.Fill; + this.textBoxUserCoordinate3.Location = new System.Drawing.Point(395, 123); + this.textBoxUserCoordinate3.MinimumSize = new System.Drawing.Size(150, 4); + this.textBoxUserCoordinate3.Name = "textBoxUserCoordinate3"; + this.textBoxUserCoordinate3.Size = new System.Drawing.Size(220, 20); + this.textBoxUserCoordinate3.TabIndex = 39; + this.textBoxUserCoordinate3.Tag = "7"; + this.textBoxUserCoordinate3.Text = "x\'3, y\'3, z\'3"; + this.textBoxUserCoordinate3.TextChanged += new System.EventHandler(this.textBoxUserCoordinate_TextChanged); + // + // textBoxUserCoordinate2 + // + this.textBoxUserCoordinate2.Dock = System.Windows.Forms.DockStyle.Fill; + this.textBoxUserCoordinate2.Location = new System.Drawing.Point(395, 93); + this.textBoxUserCoordinate2.MinimumSize = new System.Drawing.Size(150, 4); + this.textBoxUserCoordinate2.Name = "textBoxUserCoordinate2"; + this.textBoxUserCoordinate2.Size = new System.Drawing.Size(220, 20); + this.textBoxUserCoordinate2.TabIndex = 38; + this.textBoxUserCoordinate2.Tag = "6"; + this.textBoxUserCoordinate2.Text = "x\'2, y\'2, z\'2"; + this.textBoxUserCoordinate2.TextChanged += new System.EventHandler(this.textBoxUserCoordinate_TextChanged); + // + // textBoxUserCoordinate1 + // + this.textBoxUserCoordinate1.Dock = System.Windows.Forms.DockStyle.Fill; + this.textBoxUserCoordinate1.Location = new System.Drawing.Point(395, 63); + this.textBoxUserCoordinate1.MinimumSize = new System.Drawing.Size(150, 4); + this.textBoxUserCoordinate1.Name = "textBoxUserCoordinate1"; + this.textBoxUserCoordinate1.Size = new System.Drawing.Size(220, 20); + this.textBoxUserCoordinate1.TabIndex = 37; + this.textBoxUserCoordinate1.Tag = "5"; + this.textBoxUserCoordinate1.Text = "x\'1, y\'1, z\'1"; + this.textBoxUserCoordinate1.TextChanged += new System.EventHandler(this.textBoxUserCoordinate_TextChanged); + // + // textBoxUserCoordinate0 + // + this.textBoxUserCoordinate0.Dock = System.Windows.Forms.DockStyle.Fill; + this.textBoxUserCoordinate0.Location = new System.Drawing.Point(395, 33); + this.textBoxUserCoordinate0.MinimumSize = new System.Drawing.Size(150, 4); + this.textBoxUserCoordinate0.Name = "textBoxUserCoordinate0"; + this.textBoxUserCoordinate0.Size = new System.Drawing.Size(220, 20); + this.textBoxUserCoordinate0.TabIndex = 36; + this.textBoxUserCoordinate0.Tag = "4"; + this.textBoxUserCoordinate0.Text = "x\'0, y\'0, z\'0"; + this.textBoxUserCoordinate0.TextChanged += new System.EventHandler(this.textBoxUserCoordinate_TextChanged); + // + // textBoxTS4231Coordinate3 + // + this.textBoxTS4231Coordinate3.Dock = System.Windows.Forms.DockStyle.Fill; + this.textBoxTS4231Coordinate3.Location = new System.Drawing.Point(169, 123); + this.textBoxTS4231Coordinate3.MinimumSize = new System.Drawing.Size(150, 4); + this.textBoxTS4231Coordinate3.Name = "textBoxTS4231Coordinate3"; + this.textBoxTS4231Coordinate3.ReadOnly = true; + this.textBoxTS4231Coordinate3.Size = new System.Drawing.Size(220, 20); + this.textBoxTS4231Coordinate3.TabIndex = 33; + this.textBoxTS4231Coordinate3.TabStop = false; + this.textBoxTS4231Coordinate3.Tag = "3"; + this.textBoxTS4231Coordinate3.Text = "x3, y3, z3"; + this.textBoxTS4231Coordinate3.TextChanged += new System.EventHandler(this.textBoxTS4231Coordinate_TextChanged); + // + // textBoxTS4231Coordinate2 + // + this.textBoxTS4231Coordinate2.Dock = System.Windows.Forms.DockStyle.Fill; + this.textBoxTS4231Coordinate2.Location = new System.Drawing.Point(169, 93); + this.textBoxTS4231Coordinate2.MinimumSize = new System.Drawing.Size(150, 4); + this.textBoxTS4231Coordinate2.Name = "textBoxTS4231Coordinate2"; + this.textBoxTS4231Coordinate2.ReadOnly = true; + this.textBoxTS4231Coordinate2.Size = new System.Drawing.Size(220, 20); + this.textBoxTS4231Coordinate2.TabIndex = 32; + this.textBoxTS4231Coordinate2.TabStop = false; + this.textBoxTS4231Coordinate2.Tag = "2"; + this.textBoxTS4231Coordinate2.Text = "x2, y2, z2"; + this.textBoxTS4231Coordinate2.TextChanged += new System.EventHandler(this.textBoxTS4231Coordinate_TextChanged); + // + // textBoxTS4231Coordinate1 + // + this.textBoxTS4231Coordinate1.Dock = System.Windows.Forms.DockStyle.Fill; + this.textBoxTS4231Coordinate1.Location = new System.Drawing.Point(169, 63); + this.textBoxTS4231Coordinate1.MinimumSize = new System.Drawing.Size(150, 4); + this.textBoxTS4231Coordinate1.Name = "textBoxTS4231Coordinate1"; + this.textBoxTS4231Coordinate1.ReadOnly = true; + this.textBoxTS4231Coordinate1.Size = new System.Drawing.Size(220, 20); + this.textBoxTS4231Coordinate1.TabIndex = 31; + this.textBoxTS4231Coordinate1.TabStop = false; + this.textBoxTS4231Coordinate1.Tag = "1"; + this.textBoxTS4231Coordinate1.Text = "x1, y1, z1"; + this.textBoxTS4231Coordinate1.TextChanged += new System.EventHandler(this.textBoxTS4231Coordinate_TextChanged); + // + // buttonMeasure3 + // + this.buttonMeasure3.Location = new System.Drawing.Point(83, 123); + this.buttonMeasure3.Name = "buttonMeasure3"; + this.buttonMeasure3.Size = new System.Drawing.Size(80, 24); + this.buttonMeasure3.TabIndex = 29; + this.buttonMeasure3.Tag = "3"; + this.buttonMeasure3.Text = "Measure"; + this.buttonMeasure3.UseVisualStyleBackColor = true; + this.buttonMeasure3.Click += new System.EventHandler(this.buttonMeasure_Click); + // + // buttonMeasure2 + // + this.buttonMeasure2.Location = new System.Drawing.Point(83, 93); + this.buttonMeasure2.Name = "buttonMeasure2"; + this.buttonMeasure2.Size = new System.Drawing.Size(80, 24); + this.buttonMeasure2.TabIndex = 26; + this.buttonMeasure2.Tag = "2"; + this.buttonMeasure2.Text = "Measure"; + this.buttonMeasure2.UseVisualStyleBackColor = true; + this.buttonMeasure2.Click += new System.EventHandler(this.buttonMeasure_Click); + // + // buttonMeasure1 + // + this.buttonMeasure1.Anchor = System.Windows.Forms.AnchorStyles.Left; + this.buttonMeasure1.Location = new System.Drawing.Point(83, 63); + this.buttonMeasure1.Name = "buttonMeasure1"; + this.buttonMeasure1.Size = new System.Drawing.Size(80, 24); + this.buttonMeasure1.TabIndex = 23; + this.buttonMeasure1.Tag = "1"; + this.buttonMeasure1.Text = "Measure"; + this.buttonMeasure1.UseVisualStyleBackColor = true; + this.buttonMeasure1.Click += new System.EventHandler(this.buttonMeasure_Click); + // + // labelCoordinate3 + // + this.labelCoordinate3.Dock = System.Windows.Forms.DockStyle.Fill; + this.labelCoordinate3.Location = new System.Drawing.Point(3, 120); + this.labelCoordinate3.Name = "labelCoordinate3"; + this.labelCoordinate3.Size = new System.Drawing.Size(74, 30); + this.labelCoordinate3.TabIndex = 18; + this.labelCoordinate3.Text = "Coordinate 3:"; + this.labelCoordinate3.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // labelCoordinate2 + // + this.labelCoordinate2.Dock = System.Windows.Forms.DockStyle.Fill; + this.labelCoordinate2.Location = new System.Drawing.Point(3, 90); + this.labelCoordinate2.Name = "labelCoordinate2"; + this.labelCoordinate2.Size = new System.Drawing.Size(74, 30); + this.labelCoordinate2.TabIndex = 16; + this.labelCoordinate2.Text = "Coordinate 2:"; + this.labelCoordinate2.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // labelCoordinate1 + // + this.labelCoordinate1.Location = new System.Drawing.Point(3, 60); + this.labelCoordinate1.Name = "labelCoordinate1"; + this.labelCoordinate1.Size = new System.Drawing.Size(74, 30); + this.labelCoordinate1.TabIndex = 10; + this.labelCoordinate1.Text = "Coordinate 1:"; + this.labelCoordinate1.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // buttonMeasure0 + // + this.buttonMeasure0.Location = new System.Drawing.Point(83, 33); + this.buttonMeasure0.Name = "buttonMeasure0"; + this.buttonMeasure0.Size = new System.Drawing.Size(80, 24); + this.buttonMeasure0.TabIndex = 1; + this.buttonMeasure0.Tag = "0"; + this.buttonMeasure0.Text = "Measure"; + this.buttonMeasure0.UseVisualStyleBackColor = true; + this.buttonMeasure0.Click += new System.EventHandler(this.buttonMeasure_Click); + // + // labelHeaderTS4231 + // + this.tableLayoutPanelCoordinates.SetColumnSpan(this.labelHeaderTS4231, 2); + this.labelHeaderTS4231.Dock = System.Windows.Forms.DockStyle.Fill; + this.labelHeaderTS4231.Location = new System.Drawing.Point(80, 0); + this.labelHeaderTS4231.Margin = new System.Windows.Forms.Padding(0); + this.labelHeaderTS4231.Name = "labelHeaderTS4231"; + this.labelHeaderTS4231.Size = new System.Drawing.Size(312, 30); + this.labelHeaderTS4231.TabIndex = 0; + this.labelHeaderTS4231.Text = "Naive TS4231 Coordinates"; + this.labelHeaderTS4231.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // labelCoordinate0 + // + this.labelCoordinate0.Location = new System.Drawing.Point(3, 30); + this.labelCoordinate0.Name = "labelCoordinate0"; + this.labelCoordinate0.Size = new System.Drawing.Size(74, 30); + this.labelCoordinate0.TabIndex = 2; + this.labelCoordinate0.Text = "Coordinate 0:"; + this.labelCoordinate0.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // textBoxTS4231Coordinate0 + // + this.textBoxTS4231Coordinate0.Dock = System.Windows.Forms.DockStyle.Fill; + this.textBoxTS4231Coordinate0.Location = new System.Drawing.Point(169, 33); + this.textBoxTS4231Coordinate0.MinimumSize = new System.Drawing.Size(150, 4); + this.textBoxTS4231Coordinate0.Name = "textBoxTS4231Coordinate0"; + this.textBoxTS4231Coordinate0.ReadOnly = true; + this.textBoxTS4231Coordinate0.Size = new System.Drawing.Size(220, 20); + this.textBoxTS4231Coordinate0.TabIndex = 30; + this.textBoxTS4231Coordinate0.TabStop = false; + this.textBoxTS4231Coordinate0.Tag = "0"; + this.textBoxTS4231Coordinate0.Text = "x0, y0, z0"; + this.textBoxTS4231Coordinate0.TextChanged += new System.EventHandler(this.textBoxTS4231Coordinate_TextChanged); + // + // labelHeaderUser + // + this.labelHeaderUser.Dock = System.Windows.Forms.DockStyle.Fill; + this.labelHeaderUser.Location = new System.Drawing.Point(395, 0); + this.labelHeaderUser.MinimumSize = new System.Drawing.Size(150, 0); + this.labelHeaderUser.Name = "labelHeaderUser"; + this.labelHeaderUser.Size = new System.Drawing.Size(220, 30); + this.labelHeaderUser.TabIndex = 34; + this.labelHeaderUser.Text = "User-Defined Coordinates"; + this.labelHeaderUser.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // buttonCalculate + // + this.buttonCalculate.Dock = System.Windows.Forms.DockStyle.Fill; + this.buttonCalculate.Enabled = false; + this.buttonCalculate.Location = new System.Drawing.Point(3, 599); + this.buttonCalculate.Name = "buttonCalculate"; + this.buttonCalculate.Size = new System.Drawing.Size(618, 23); + this.buttonCalculate.TabIndex = 7; + this.buttonCalculate.Text = "Calculate Spatial Transform"; + this.buttonCalculate.UseVisualStyleBackColor = true; + this.buttonCalculate.Click += new System.EventHandler(this.buttonCalculate_Click); + // + // flowLayoutPanelBottom + // + this.flowLayoutPanelBottom.AutoSize = true; + this.flowLayoutPanelBottom.Controls.Add(this.buttonClose); + this.flowLayoutPanelBottom.Controls.Add(this.checkBoxApplySpatialTransform); + this.flowLayoutPanelBottom.Dock = System.Windows.Forms.DockStyle.Fill; + this.flowLayoutPanelBottom.FlowDirection = System.Windows.Forms.FlowDirection.RightToLeft; + this.flowLayoutPanelBottom.Location = new System.Drawing.Point(3, 628); + this.flowLayoutPanelBottom.Name = "flowLayoutPanelBottom"; + this.flowLayoutPanelBottom.Size = new System.Drawing.Size(618, 30); + this.flowLayoutPanelBottom.TabIndex = 8; + // + // buttonClose + // + this.buttonClose.DialogResult = System.Windows.Forms.DialogResult.OK; + this.buttonClose.Location = new System.Drawing.Point(535, 3); + this.buttonClose.Name = "buttonClose"; + this.buttonClose.Size = new System.Drawing.Size(80, 24); + this.buttonClose.TabIndex = 0; + this.buttonClose.Text = "Close"; + this.buttonClose.UseVisualStyleBackColor = true; + this.buttonClose.Click += new System.EventHandler(this.buttonClose_Click); + // + // checkBoxApplySpatialTransform + // + this.checkBoxApplySpatialTransform.AutoSize = true; + this.checkBoxApplySpatialTransform.Dock = System.Windows.Forms.DockStyle.Fill; + this.checkBoxApplySpatialTransform.Enabled = false; + this.checkBoxApplySpatialTransform.Location = new System.Drawing.Point(268, 3); + this.checkBoxApplySpatialTransform.Name = "checkBoxApplySpatialTransform"; + this.checkBoxApplySpatialTransform.Size = new System.Drawing.Size(261, 24); + this.checkBoxApplySpatialTransform.TabIndex = 1; + this.checkBoxApplySpatialTransform.Text = "Set SpatialTransformMatrix property when closing."; + this.checkBoxApplySpatialTransform.UseVisualStyleBackColor = true; + this.checkBoxApplySpatialTransform.CheckedChanged += new System.EventHandler(this.checkBoxApplySpatialTransform_CheckedChanged); + // + // SpatialTransformMatrixDialog + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(624, 661); + this.Controls.Add(this.tableLayoutPanelMain); + this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); + this.MinimumSize = new System.Drawing.Size(640, 700); + this.Name = "SpatialTransformMatrixDialog"; + this.Text = "TS4231V1 Calibration GUI"; + this.tableLayoutPanelMain.ResumeLayout(false); + this.tableLayoutPanelMain.PerformLayout(); + this.groupBoxStatus.ResumeLayout(false); + this.groupBoxStatus.PerformLayout(); + this.tableLayoutPanelCoordinates.ResumeLayout(false); + this.tableLayoutPanelCoordinates.PerformLayout(); + this.flowLayoutPanelBottom.ResumeLayout(false); + this.flowLayoutPanelBottom.PerformLayout(); + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.TableLayoutPanel tableLayoutPanelMain; + private System.Windows.Forms.GroupBox groupBoxStatus; + private System.Windows.Forms.TextBox textBoxStatus; + private System.Windows.Forms.TableLayoutPanel tableLayoutPanelCoordinates; + private System.Windows.Forms.TextBox textBoxUserCoordinate3; + private System.Windows.Forms.TextBox textBoxUserCoordinate2; + private System.Windows.Forms.TextBox textBoxUserCoordinate1; + private System.Windows.Forms.TextBox textBoxUserCoordinate0; + private System.Windows.Forms.TextBox textBoxTS4231Coordinate3; + private System.Windows.Forms.TextBox textBoxTS4231Coordinate2; + private System.Windows.Forms.TextBox textBoxTS4231Coordinate1; + private System.Windows.Forms.Button buttonMeasure3; + private System.Windows.Forms.Button buttonMeasure2; + private System.Windows.Forms.Button buttonMeasure1; + private System.Windows.Forms.Label labelCoordinate3; + private System.Windows.Forms.Label labelCoordinate2; + private System.Windows.Forms.Label labelCoordinate1; + private System.Windows.Forms.Button buttonMeasure0; + private System.Windows.Forms.Label labelHeaderTS4231; + private System.Windows.Forms.Label labelCoordinate0; + private System.Windows.Forms.TextBox textBoxTS4231Coordinate0; + private System.Windows.Forms.Label labelHeaderUser; + private System.Windows.Forms.Button buttonCalculate; + private System.Windows.Forms.FlowLayoutPanel flowLayoutPanelBottom; + private System.Windows.Forms.Button buttonClose; + private System.Windows.Forms.CheckBox checkBoxApplySpatialTransform; + private System.Windows.Forms.Label labelInstructions; + } +} diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs new file mode 100644 index 00000000..154c64c8 --- /dev/null +++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs @@ -0,0 +1,140 @@ +using System; +using System.Linq; +using System.Numerics; +using System.Windows.Forms; +using System.Reactive.Linq; +using Bonsai.Design; + +namespace OpenEphys.Onix1.Design +{ + public partial class SpatialTransformMatrixDialog : Form + { + private const byte NumMeasurements = 100; + + private bool[] InputsValid = { false, false, false, false, false, false, false, false }; + + private IObservable> PositionDataSource; + + private Vector3[] TS4231Coordinates = { Vector3.Zero, Vector3.Zero, Vector3.Zero, Vector3.Zero }; + + internal Matrix4x4 SpatialTransform { get; private set; } + + internal bool ApplySpatialTransform { get; private set; } + + internal SpatialTransformMatrixDialog(IObservable> positionDataSource) + { + InitializeComponent(); + PositionDataSource = positionDataSource; + } + + private void buttonMeasure_Click(object sender, EventArgs e) + { + TextBox[] ts4231TextBoxes = { textBoxTS4231Coordinate0, textBoxTS4231Coordinate1, textBoxTS4231Coordinate2, textBoxTS4231Coordinate3 }; + var index = int.Parse((string)((Button)sender).Tag); + + textBoxStatus.AppendText(string.Format("Measuring coordinate {0}...", index) + Environment.NewLine); + + buttonMeasure0.Enabled = false; + buttonMeasure1.Enabled = false; + buttonMeasure2.Enabled = false; + buttonMeasure3.Enabled = false; + buttonCalculate.Enabled = false; + + var sharedPositionDataGroups = PositionDataSource.Take(NumMeasurements) + .GroupBy(dataFrame => dataFrame.Item1, dataFrame => dataFrame.Item2) + .Publish(); + + sharedPositionDataGroups + .SelectMany(group => group.Count().Select(count => new { index = group.Key, measurementCount = count })) + .ObserveOn(new ControlScheduler(this)) + .Finally(() => + { + textBoxStatus.AppendText(string.Format("Measurements at coordinate {0} complete.", index) + + Environment.NewLine + Environment.NewLine + "Awaiting user input..." + Environment.NewLine); + buttonMeasure0.Enabled = true; + buttonMeasure1.Enabled = true; + buttonMeasure2.Enabled = true; + buttonMeasure3.Enabled = true; + buttonCalculate.Enabled = InputsValid.All(inputValid => inputValid); + }) + .Subscribe(sensor => + { + textBoxStatus.AppendText(string.Format("{1} measurements from sensor {0}.", sensor.index, sensor.measurementCount) + Environment.NewLine); + }); + + sharedPositionDataGroups + .Merge() + .ObserveOn(new ControlScheduler(this)) + .Aggregate( + new Vector3(0, 0, 0), + (acc, current) => acc + current, + acc => + { + TS4231Coordinates[index] = acc / NumMeasurements; + ts4231TextBoxes[index].Text = string.Format("{0}, {1}, {2}", + TS4231Coordinates[index].X, + TS4231Coordinates[index].Y, + TS4231Coordinates[index].Z); + return TS4231Coordinates[index]; + }) + .Subscribe(); + + sharedPositionDataGroups.Connect(); + } + + private void textBoxTS4231Coordinate_TextChanged(object sender, EventArgs e) + { + var index = int.Parse((string)((TextBox)sender).Tag); + InputsValid[index] = true; + buttonCalculate.Enabled = InputsValid.All(inputValid => inputValid); + } + + private void textBoxUserCoordinate_TextChanged(object sender, EventArgs e) + { + var index = int.Parse((string)((TextBox)sender).Tag); + string[] serInputSplit = ((TextBox)sender).Text.Split(','); + InputsValid[index] = serInputSplit.Length == 3 ? serInputSplit.All(floatCandidate => float.TryParse(floatCandidate, out _)) : false; + buttonCalculate.Enabled = InputsValid.All(inputValid => inputValid); + } + + private void buttonCalculate_Click(object sender, EventArgs e) + { + var ts4231V1CoordinatesMatrix = new Matrix4x4( + TS4231Coordinates[0].X, TS4231Coordinates[0].Y, TS4231Coordinates[0].Z, 1, + TS4231Coordinates[1].X, TS4231Coordinates[1].Y, TS4231Coordinates[1].Y, 1, + TS4231Coordinates[2].X, TS4231Coordinates[2].Y, TS4231Coordinates[2].Z, 1, + TS4231Coordinates[3].X, TS4231Coordinates[3].Y, TS4231Coordinates[3].Z, 1); + + float[][] userCoordinates = { + textBoxUserCoordinate0.Text.Split(',').Select(item => float.Parse(item)).ToArray(), + textBoxUserCoordinate1.Text.Split(',').Select(item => float.Parse(item)).ToArray(), + textBoxUserCoordinate2.Text.Split(',').Select(item => float.Parse(item)).ToArray(), + textBoxUserCoordinate3.Text.Split(',').Select(item => float.Parse(item)).ToArray()}; + + var userCoordinatesMatrix = new Matrix4x4( + userCoordinates[0][0], userCoordinates[0][1], userCoordinates[0][2], 1, + userCoordinates[1][0], userCoordinates[1][1], userCoordinates[1][2], 1, + userCoordinates[2][0], userCoordinates[2][1], userCoordinates[2][2], 1, + userCoordinates[3][0], userCoordinates[3][1], userCoordinates[3][2], 1); + + Matrix4x4.Invert(ts4231V1CoordinatesMatrix, out var ts4231V1CoordinatesMatrixInverted); + SpatialTransform = Matrix4x4.Multiply(ts4231V1CoordinatesMatrixInverted, userCoordinatesMatrix); + + textBoxStatus.AppendText("The spatial transform matrix for the above coordinates is:" + Environment.NewLine); + textBoxStatus.AppendText(SpatialTransform.ToString() + Environment.NewLine + Environment.NewLine); + textBoxStatus.AppendText("Awaiting user input..." + Environment.NewLine); + + checkBoxApplySpatialTransform.Enabled = true; + } + + private void buttonClose_Click(object sender, EventArgs e) + { + Close(); + } + + private void checkBoxApplySpatialTransform_CheckedChanged(object sender, EventArgs e) + { + ApplySpatialTransform = checkBoxApplySpatialTransform.Checked; + } + } +} diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.resx b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.resx new file mode 100644 index 00000000..b6de2ecd --- /dev/null +++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.resx @@ -0,0 +1,1784 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Follow the instructions below to transform naive TS4231 position data from a naive reference frame to a user-defined reference frame. For more in-depth instructions, find the corresponding tutorial in Open Ephys' online documentation. +1) Make sure the workflow is running. +2) For each coordinate: + • Place the TS4231V1 device and click the corresponding "Measure" button. + • Input how would like to define the coordinate in the user-defined reference frame. +3) Click the "Calculate Spatial Transform" button which enables after all fields are populated with valid data. +4) To automatically set the SpatialTransformMatrix property, check the bottom checkbox and close this GUI. + + + + + AAABAA4AEBAQAAEABAAoAQAA5gAAABAQAAABAAgAaAUAAA4CAAAQEAAAAQAgAGgEAAB2BwAAICAQAAEA + BADoAgAA3gsAACAgAAABAAgAqAgAAMYOAAAgIAAAAQAgAKgQAABuFwAAMDAQAAEABABoBgAAFigAADAw + AAABAAgAqA4AAH4uAAAwMAAAAQAYAKgcAAAmPQAAMDAAAAEAIACoJQAAzlkAAEBAAAABABgAKDIAAHZ/ + AABAQAAAAQAgAChCAACesQAAAAAAAAEAGABpMQAAxvMAAAAAAAABACAAZ10AAC8lAQAoAAAAEAAAACAA + AAABAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAACAgAAAgICAAACAgADAwMAA//8AAAD/ + /wAAAP8A/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUiAAAAAAAFU5YAAA + AAVVADYgAAAFVQAGYlAAVTUAVTUjMAM1VVVTIDOABVAAAAYDRHAAVQAAYzhlAAAFUAY4AFcAAABVA0AA + NwAAAAUDAAZAAAAAAAAAA0AAAAAAAAACAAAAAAAAAAEAAAAAAAAAAAAA//8AAP/xAAD/wQAA/jEAAPjh + AADDAAAAgBEAAJ+hAADPAwAA5jMAAPJzAAD45wAA/+cAAP/vAAD/7wAA//8AACgAAAAQAAAAIAAAAAEA + CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAI5YzAEWaLgCFpi8AJLioAL2exQA9QTAAO12VANK0 + LwAws5AAzarWAGZsPgA+Z9UAdLRpACbQ+wDdtS8Ak41jAEBr6QDAtkIA37YvAKmdbgA/auoAR27gANKu + MgA8zeMAu5vDAMiq0ABLXUwATaKmAHpvfACWlJUAqqmpALe2tgDGr2QAxKAqAHt6RQA+XowAHJXwALW0 + tACysbEAtLOzALe1swCrjzcAsp5eAK2ecQCQgU8ALk93AA09OADctDEAzqw0AN61LgDbrikA3bMsAMWs + XgC/gxAA1qYjALazrQCMYSEAnGUNAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADg5 + OgAAAAAAAAAAAAAAICA1NjcAAAAAAAAAACAgIAAAMjM0AAAAAAAAICAgAAAAExMwMQAAACYnKCAAACAp + KissLS4vAB0eHyAgICAgISIAIyQlAAAZGgAAAAAAABMAFRscDgAAAAoKAAAAABMUFRYXGAAAAAAACgoA + AA8QEQAAEg4AAAAAAAAKCgALDAAAAA0OAAAAAAAAAAUGBwAAAAgJAAAAAAAAAAAAAAAAAAADBAAAAAAA + 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 + ABMAAAAAAAAAAAAAAAAAAAASAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//////// + //////+H///+B///+AP//8AD//8HA//4HgP/4HwB/wP8ifwP+AHwAAABgAABAYAH5wOH/8YD4/+MB/H/ + kAf4/wEH/H4DD/48Dg//HB4f/4h+H//A/h//4f4///f+P////H////x////8f////P////z///////// + //8oAAAAIAAAAEAAAAABAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACGULgAmmTwAIpQuACGX + NwBNnC4AIZQvACW/wACPqC8AJLmsANS0LwAxly4AI6+JAD5CNwB1oy4AIqNhACbQ+wCchaAALDMkAC02 + KQA8X6cAwbEvACOZPgAmz/cAzarWAMmn0QBmWUwAM0MoADhQRQA+aN0A37YvAFKdLgAlyN8Ano0tAEFS + KwA6V3AAP2rpAJ+qLwAkvLcAxqYuAFliLAA8X6QAP2rqANm1LwA1tJAA2bIvAIF8OAA/Z9AAfLVpAKyb + VgBDbOUAxbZAACbQ+gDCqFEAPszgAEFr5gCrlzoAf8OZADpUWgCAei0AzLE5AIhxjgCkiKsAt5e/AMKn + yQA6WX8AN0wrAEFXUAAsuPcAaVZtAHZpeQCbmZoAsK+vALe2tgBWaGgAPE8rADpt6QAcpPMAdGJ3AIB2 + ggCKiYkAhoWFAIWEhACTkpIAqKenALa1tQCynl4AsI8lALSSJgCxp4oAwLKGAKqRKwBIVSwAPWXVABlb + 5AAUg+0AJMf5ALOysgCrqqoArq2tALazrwCkizoAoIIiAKSMPgC2tLEAoYQpAJl9IgBZZocAIkmpAAY3 + aAALRk0Au5goALaVJwC4pGMAp44+AKKEIgCsnnMAgIqoABAyJAAGLB4A3LMuANayOgC0mSoAmIsqAN60 + LQDftS8A3rQuANOgHwDarSgA2aomANK0VADetS8AyY0RAMyTFgCwlUEAtYMTALJqBQDNlRcAs62dAJdy + HAB9RAEAo2kMAKSQawB6QgEAgUoFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAASY6PkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASUmKi4yNAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAASUlJaIaHiIkeAAAAAAAAAAAAAAAAAAAAAAAAAABJSUlJSUmCg4SFfx4AAAAA + AAAAAAAAAAAAAAAAAABJSUlJSQAAAB58f4CBHgAAAAAAAAAAAAAAAAAAAElJSUlJSQAAAAAeHnx9Hn4e + AAAAAAAAAAAAAAAAAElJSUlJSQAAAAAAHh4eHh4eHh4eAAAAAAAAAAAASUlJSUlJAAAAAAAAAAAeHgAe + eHkAensAAAAAAAAASUlJSUlJAAAAAAAAAAAAb3BxSXJzdHV2dwAAAAAAVWFIYmNVSUlJSUlJSUlJZGVm + Z2hoaWprbG1uAABOT1BRUlNUVUlJSUlJSUlJSUlWV1hZAFpbXF1eX2AAAEVFRkdISUlJSUlJSQAAAAAA + AB4eAAAASkscTE0QAAAAPT4/QAAAAAAAAAAAAAAAAAAeHh4AACpBQkNEEBAAAAAAABgYGAAAAAAAAAAA + AAAAHh4eAAAqKjo7PBAQAAAAAAAAABgYGAAAAAAAAAAAAAAeHgAqKio3OB45EBAAAAAAAAAAABgYGAAA + AAAAAAAAHh41KioqKgAeHjYQEAAAAAAAAAAAABgYGAAAAAAAAB4eMTIqKioAAB4zNBAAAAAAAAAAAAAA + ABgYGAAAAAAeLS4vKioAAAAeHjAQEAAAAAAAAAAAAAAAABgYGAAAACcoKSoqAAAAAB4rLBAAAAAAAAAA + AAAAAAAAABgYGAAhIiMkAAAAAAAAHiUmEAAAAAAAAAAAAAAAAAAAABgZGhscHQAAAAAAAAAeHyAQAAAA + AAAAAAAAAAAAAAAAABESExQAAAAAAAAAABUWFwAAAAAAAAAAAAAAAAAAAAAAAA0AAAAAAAAAAAAADg8Q + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoLDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + CAEJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFBgcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAMEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQIAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///////////// + /4f///4H///4A///wAP//wcD//geA//gfAH/A/yJ/A/4AfAAAAGAAAEBgAfnA4f/xgPj/4wH8f+QB/j/ + AQf8fgMP/jwOD/8cHh//iH4f/8D+H//h/j//9/4////8f////H////x////8/////P///////////ygA + AAAgAAAAQAAAAAEAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsqqXDIxaEWuPWw1DAAAAAAAAAAAAAAAAAAAAAAAA + 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 + AADwf//DAI8AAPg//4YADwAA/B//DBAfAAD+D/8AMB8AAP8H/gBwPwAA/8P8AeA/AAD/4fgD4D8AAP/w + eAfgfwAA//gwH+B/AAD//AA/4P8AAP/+AP/A/wAA//8B/8D/AAD//4P/wf8AAP//z//B/wAA/////8P/ + AAD/////g/8AAP////+D/wAA/////4f/AAD/////h/8AAP////+P/wAA/////w//AAD/////j/8AAP// + /////wAA////////AAD///////8AACgAAAAwAAAAYAAAAAEACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAIZQuAC2cQQA4nT8AI5xHACSVLgAhlzgAUp0uACGULwAlwMQAlqkvACS4qADXtS8AMZcuACOs + gQAm0PsA37YvAHmkLgAioVoAJtD6AMCwLwAmlS4AJs71AD1BNwA7QDQAVJ0uACXF1wCJd4sALDMkAC44 + LwA8YbUAoqsvACS6rwDNqtYAq5CwAC81KAAvOyYAOFJRAD9p4gDYtS8AN5guACOviQCbfXkAQEMmADZL + KwA3TCsAOll/AD9q6QB9pC4AIqNgAMypzQDHpS8AaGssADhMKwA3TC0APGGyAD9q6gDGsi8AJptCACbN + 8gDbtC8AkoYtADtPKwA4T0AAPmfWAFieMAAlx9wAu58uAFBbLAA5VWIAP2roAKWsLwAlvLcA0q4vAHFx + LQA8XZUA2bUvAD61jwDetS8Am4w1AEVpxACCtmkAwKZJAEpv3QDItj0AJ9D5ANKwPgBAaukA3rYvAELM + 3ACFw5IAP2rmAHV3SADUry8A2bc2ADpWaABTXiwAvaAuADtbjQA9UCsAioEvAIBqhQCKcpAAoISnAK+R + tgC7ocEAPWGxADhNLQA+ZtMAKcP5AHdlewBpVm0AeWp8AKGeoAC0s7MAt7a2ADlOLQA9YrkANHLrAB+y + 9gB2ZHoAcmV0AIKBgQCDgoIAh4aGAJqZmQCura0AyrFhANCpLADUrS0A2bEuANy0LgCymS4ATVorADtc + kgA+auoAElznABiU8QAlzfsAeWh8AIh+iQCRkJAAjYyMAImIiACGhYUAhYSEAISDgwCmpaUAtbS0ALe1 + tACnkU0AoYMiAKKEIgCkhSMArqB3AMCjRQC4mCkAXmEoADdSaQA9ZuIAJl/kAAlV5AARdusAIsH4ALGw + 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 + AAAAAAAAAAAAAAAAAAAAAAAAISEhISEAAAAAEENERUY4ODgAAAAAAAAQEEdIDw8AAAAAAAAAAAAAAAAA + AAAAAAAAACEhISEhAAA8PT4/QDg4AAAAAAAAAAAQEEFCDw8AAAAAAAAAAAAAAAAAAAAAAAAAAAAhISEh + MjM0NTY3ODgAAAAAAAAAAAAQOTo7DwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAISEhKissLS4vAAAAAAAA + AAAAABAQMDEPDwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACEiIxwkJSYAAAAAAAAAAAAAABAnKCkPDwAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbHBwdHgAAAAAAAAAAAAAAABAfASAPAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAFxgAAAAAAAAAAAAAAAAAABAZARoPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAABQVBhYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAEBEBEhMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADA0BDg8AAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgEBCwAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwEICQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAABQEGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAADAQEEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQECAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////////AAD///////8AAP//////nwAA//////4f + AAD/////8A8AAP/////ADwAA/////gAPAAD////4AAcAAP///+AQBwAA////AHAHAAD///wD4AcAAP// + 4A/AAwAA//+AP8AjAAD//AH/hCMAAP/wB/8MIQAA/8A//gABAAD+AP4AAAEAAPgAAAAAAQAAwAAAAAAD + AACAAAAQeAMAAIAA//D4BwAAwf//4eAHAADg///DwIcAAPB//8MAjwAA+D//hgAPAAD8H/8MEB8AAP4P + /wAwHwAA/wf+AHA/AAD/w/wB4D8AAP/h+APgPwAA//B4B+B/AAD/+DAf4H8AAP/8AD/g/wAA//4A/8D/ + AAD//wH/wP8AAP//g//B/wAA///P/8H/AAD/////w/8AAP////+D/wAA/////4P/AAD/////h/8AAP// + //+H/wAA/////4//AAD/////D/8AAP////+P/wAA////////AAD///////8AAP///////wAAKAAAADAA + AABgAAAAAQAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIpWC49bDQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + 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 + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFKdLiGULiGULyXAxAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + ACSVLiGULiGXOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAADidPyGULiGULiOcRwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACGULiGULi2cQQAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///////wAA//////// + AAD//////58AAP/////+HwAA//////APAAD/////wA8AAP////4ADwAA////+AAHAAD////gEAcAAP// + /wBwBwAA///8A+AHAAD//+APwAMAAP//gD/AIwAA//wB/4QjAAD/8Af/DCEAAP/AP/4AAQAA/gD+AAAB + AAD4AAAAAAEAAMAAAAAAAwAAgAAAEHgDAACAAP/w+AcAAMH//+HgBwAA4P//w8CHAADwf//DAI8AAPg/ + /4YADwAA/B//DBAfAAD+D/8AMB8AAP8H/gBwPwAA/8P8AeA/AAD/4fgD4D8AAP/weAfgfwAA//gwH+B/ + AAD//AA/4P8AAP/+AP/A/wAA//8B/8D/AAD//4P/wf8AAP//z//B/wAA/////8P/AAD/////g/8AAP// + //+D/wAA/////4f/AAD/////h/8AAP////+P/wAA/////w//AAD/////j/8AAP///////wAA//////// + AAD///////8AACgAAAAwAAAAYAAAAAEAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + 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 + ogYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC3trYyt7a2mre2tu+3trb/t7a2/7e2tv+3trb/t7a2/7e2 + tvm3traqt7a2Q7e2tgO3trYIt7a2ILe2tji3trZPt7a2aLe2toC3traXt7a2sLe2tsm3trbat7a27Le2 + tvqwp4v/oIIj/6CCIv+ggiL/pY5D/7a0sf+3trb/r6WG/6CCIv+ggiL/oYUq/5udpv9EXqr/FD1//wYs + Hv8GLB7/DDQp7WSTmw0AAAAAAAAAAAAAAAC3trYNt7a2XLe2tsa3trb+tLOz/7GwsP+ura3/qaio/6Sj + o/+mpaX/srGx9Le2tsC3tra/t7a21be2tu+3trb+t7a2/re2tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2 + tv+3trb/t7a2/7Swpf+ihzH/oIIi/6CCIv+hhSv/tK+h/7e2tv+3trb/q5to/6CCIv+fgSL/bWU3/zJQ + p/8qTKr/CECp/wY5bf8JQ1b/GXyMigAAAAAAAAAAkoKVGXlofJOIfonkkZCQ/42MjP+JiIj/hoWF/4WE + hP+Eg4P/kZCQ/6alpf+1tLT/t7a2/7e2tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2 + tv+3trb/t7a2/7e2tv+3trb/t7W0/6eRTf+hgyL/ooQi/6SFI/6uoHfgt7a2xLe2tq63traWwKNF4riY + Kf9eYSj/N1Jp/z1m4v8mX+T/CVXk/xF26/8iwfj5JtD7JgAAAAAAAAAAdmR6sWlWbf9pVm3/cmV0/4KB + gf+DgoL/h4aG/5qZmf+ura3/t7a2/7e2tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2 + tv+3trb5t7a25be2ts23tra3t7a2n7e2toe3trZvyrFhudCpLP/UrS3/2bEu/9y0LqTfti8CAAAAAAAA + AAAAAAAAspku501aK/83TCv/O1yS/z5q6v8SXOf/GJTx/yXN+/8m0Pu4JtD7AgAAAAAAAAAAd2V7xmlW + bf9pVm3/aVZt/3lqfP+hnqD/tLOz/7e2tv+3trb+t7a287e2tua3trbYt7a2wLe2tqm3traQt7a2eLe2 + tmG3trZIt7a2Mbe2thi3trYGt7a2A7e2tgEAAAAAAAAAAAAAAADfti8z37Yv99+2L//fti//37Yv2d+2 + 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 + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAN+2LwHXtS/DMZcu/yGU + Lv8jrIH/JtD7oQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAN+2 + LwyWqS/qIZQu/yGULv8kuKj/JtD7OgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAANW0LydSnS7/IZQu/yGUL/8lwMTOJtD7AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAIClL1kklS7/IZQu/yGXOP8lx9xqAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADidP5QhlC7/IZQu/yOcR/Am0PsUAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADefRX8hlC7/IZQu/y2c + QaEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFKr + YQgxnD9zLZo6fFeuZhQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///////8AAP///////wAA//////4PAAD/////+A8AAP// + ///ADwAA/////wAHAAD////4AAcAAP///+AABwAA////gAAHAAD///wAAAMAAP//8ADAAwAA//+AA4AD + AAD//gAPgAEAAP/4AH8AAQAA/8AD/gABAAD/AA/gAAAAAPwAAAAAAAAA4AAAAAABAACAAAAAAAEAAIAA + AAA4AQAAgAAA4GADAACAB//AwAMAAMB//8GABwAA4D//gAAHAADwH/8AAA8AAPgP/gAADwAA/Af+AAAP + AAD+A/wAIB8AAP8A+ADgHwAA/4BwAcA/AAD/4DADwD8AAP/wAA/APwAA//gAH8B/AAD//AA/wH8AAP/+ + AP/A/wAA//8B/4D/AAD//4P/gP8AAP//z/+B/wAA/////4H/AAD/////A/8AAP////8D/wAA/////wP/ + AAD/////B/8AAP////8H/wAA/////w//AAD/////D/8AAP///////wAA////////AAAoAAAAQAAAAIAA + AAABABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjV0YiVQKi1YLAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAt7a2n4Ve + ekIBekIBekIBh1IIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAt7a2 + t7a2t7a2squWjWEUekIBekIBekIBiVQJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAt7a2t7a2t7a2t7a2trOupIs9i14LekIBekIBekIBpG8PAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAt7a2t7a2t7a2t7a2t7a2t7W1qZddn4EhjF0HfEUBhUgBnloDz5kb37YvAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAt7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2r6SCoIMkoX8eomwIqGMErF8Cv3wI1qUj + 37YvAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAt7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2tK+grpAzw58pzp0gw4IJ + unQGuXMGw4IJ268q37YvAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAt7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2uLaz0LBH + 3bQv37Yv0Joaw4IJyY0SxIMKxocN3rQu37Yv37YvAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAt7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2 + t7a2AAAAAAAA37Yv37Yv37UvypATx4kO3LAryY4Ry5AT37Yv37Yv37YvAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAt7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2 + t7a2t7a2AAAAAAAAAAAAAAAA37Yv37Yv37Yv3rQtxogO2Kgl37Yv0Zwc0Joa37Yv37Yv37YvAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAt7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2 + t7a2t7a2t7a2t7a2AAAAAAAAAAAAAAAAAAAA37Yv37Yv37Yv37Yv27Aq0Jsb37Yv37Yv2asn1aMh37Yv + 37Yv37YvAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAt7a2t7a2t7a2t7a2t7a2t7a2 + t7a2t7a2t7a2t7a2t7a2AAAAAAAAAAAAAAAAAAAAAAAAAAAA37Yv37Yv37Yv37Yv37Yv264p3rQu37Yv + 37Yv3rUu268q37Yv37Yv37Yv37YvAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAt7a2t7a2t7a2t7a2t7a2 + t7a2t7a2t7a2t7a2t7a2t7a2t7a2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA37Yv37Yv37Yv37Yv + 37Yv37Yv37Yv37Yv37Yv37Yv37Yv37Yv37Yv37Yv37YvAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAt7a2t7a2t7a2 + t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA37Yv + 37Yv37Yv37Yv37YvAAAA37Yv37Yv37Yv37Yv37YvAAAA37Yv37Yv37Yv37YvAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAt7a2 + t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAA37Yv37Yv37Yv37Yv37Yv37YvAAAA37Yv37Yv37Yv37Yv37YvAAAA37Yv37Yv37Yv37Yv37Yv + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAA37Yv37Yv37Yv37Yv37Yv37YvAAAAAAAA37Yv37Yv37Yv37YvAAAAAAAA37Yv + 37Yv37Yv37Yv37YvAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAt7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2AAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA37Yv37Yv37Yv37Yv37YvAAAAAAAAAAAA3rUv2rIu1q8t + 0qwuAAAAAAAAv6hesJMnYWclOlIjdXcnAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAt7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2AAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA168t1a4t0KosyqUqxKAqw61ot7a2t7a2 + r51kp4gjpYYjo4Ujo4gvtbGpt7a2l5iXLEAjBiweBiweBy0eAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAt7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2AAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAt7a2t7a2t7a2t7WzqZNLpIUjoIIioIIioIIi + ppBKtrSwt7a2t7a2qJNToIIioIIioIIipIw+tbS1gIywMlCkCjAzBiweBiweBiweHkc/AAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAt7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2AAAAAAAA + AAAAAAAAAAAAAAAAt7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2rqF6oIIi + oIIioIIioIIioocvtK+it7a2t7a2t7W0o4k2oIIioIIioIIino5aXXGsLUynHkamBjRgBiweBiweBi0g + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAt7a2t7a2t7a2tLOzsrGxrq2tq6qqp6amoqGhnJubnp2dq6qq + tLOzt7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2 + t7a2sqyYoYUroIIioIIioIIioIIjr6WFt7a2t7a2t7a2tLCkoYUqoIIioIIigm8hPE5uLUynLk6rD0Os + BkGrBzt4CEBgF4SdAAAAAAAAAAAAAAAAAAAAg3SGkImRmZeYlZSUkI+Pi4qKiYiIh4aGhYSEg4KChIOD + j46OpKOjs7Kyt7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2 + t7a2t7a2t7a2t7a2trOtpY1CoIIioIIioIIioIIiqZZct7a1t7a2t7a2t7a2t6uHuZcouZgoeXEoN0gn + OV2zPGbgMWPiClXiCVXkDmzqILf3Js/7AAAAAAAAAAAAAAAAcmB2aVZtaVZtcmV0goCBg4KCg4KCg4KC + g4KChoWFmJeXrKurtrW1t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2 + t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2uKNitpUnvJkowp4pyKMqyqUsAAAAAAAAAAAAAAAAAAAAAAAA + zasvamwsOEwrN042PmfYP2rqH17oCVjmFIXuJMf6JtD7AAAAAAAAAAAAAAAAAAAAaVZtaVZtaVZtaVZt + cGJzgX+Bg4KCjIuLoJ+fs7Kyt7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2t7a2 + t7a2t7a2t7a2t7a2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA37Yv37Yv37Yv37Yv37YvAAAAAAAAAAAA + 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 + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA3rYvV54uIZQuIZQuJcPP + JtD7AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwrEv + KZUuIZQuIZUxJs70JtD7AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAA37Yve6QuIZQuIZQuIp9SJtD6AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAA17UvM5cuIZQuIZQuI6p6JtD7AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAnaovIZQuIZQuIZQuJLWgAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAV54uIZQuIZQuIZUwJcDDAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJ5UuIZQu + IZQuIZc5JcXXAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAARZ46IZQuIZQuIZQuIpxIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAM5xAIZQuIZQuIZQuJ6BTAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOaBHIZQuIZQuIpQvAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOJ9GI5UwAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAA/////////////////////////////////////////H/////////wP/// + /////4A////////+AD////////gAH///////wAAf//////8AAB//////+AAAD//////gAYAP/////4AP + AA/////8AD4AD/////AB/AAH////gAf8AAf///4AP/ggh///+AD/8CCD///AA//gYYP//wAf/+Dhg//4 + AH//wAAD/+AD//gAAAH/AA/AAAAAA/wAAAAAAAAD4AAAAAAAAAPAAAAAAA/AB8AAAAP8H4AHwAAP//g/ + AA/AP///8D4AD+A////wfAYP+B///+DwBB/8D///wOAEH/4H//+BwAQ//wP//4MAAD//gf//BgMAP//A + f/4EBwB//+A//gAOAH//8B/8AD4A///4D/gAfgD///wH8AD+Af///gPwA/4B////AeAH/AH////AwA/8 + A////+AAP/wD////8AB//Af////4Af/8B/////wD//gH/////gf/+A//////H//4D/////////gf//// + ////+B/////////4H/////////A/////////8D/////////wf/////////B/////////8H/////////g + /////////+D/////////4f/////////z//////////////////////////////////8oAAAAQAAAAIAA + AAABACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADKpUACs4ckEMObMAgAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACyqpcwjV0YpYlU + CvSLVgvSmWcTMQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAt7a2Dre2 + tli3trbBn4Ve/HpCAf96QgH/ekIB/4dSCM3FnzkGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALe2 + tgK3trYlt7a2g7e2tt63trb9squW/41hFP96QgH/ekIB/3pCAf+JVAn4yZ0oIwAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAt7a2Bre2tkW3trast7a28re2tv+3trb/trOu/6SLPf+LXgv/ekIB/3pCAf96QgH/pG8P/9+2 + L1QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAC3trYVt7a2b7e2ttW3trb+t7a2/7e2tv+3trb/t7W1/6mXXf+fgSH/jF0H/3xF + Af+FSAH/nloD/8+ZG//fti+iAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAt7a2BLe2tjm3trabt7a277e2tv+3trb/t7a2/7e2tv+3trb/t7a2/6+k + gv+ggyT/oX8e/6JsCP+oYwT/rF8C/798CP/WpSP/37Yv4N+2LwoAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC3trYSt7a2Xre2tsO3trb4t7a2/7e2tv+3trb/t7a2/7e2 + tv+3trb/t7a2/7SvoP+ukDP/w58p/86dIP/Dggn/unQG/7lzBv/Dggn/268q/9+2L/vfti88AAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAt7a2Are2tiS3traJt7a24re2tv+3trb/t7a2/7e2 + tv+3trb/t7a2/7e2tv+3trb/t7a2+Li2s7TQsEfr3bQv/9+2L//Qmhr/w4IJ/8mNEv/Egwr/xocN/960 + Lv/fti//37YvgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC3trYHt7a2T7e2trO3trb6t7a2/7e2 + tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2tv+3trbgt7a2g7m2sSLfti9937Yv/9+2L//ftS//ypAT/8eJ + Dv/csCv/yY4R/8uQE//fti//37Yv/9+2L8wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALe2tiG3trZ2t7a22be2 + tv23trb/t7a2/7e2tv+3trb/t7a2/7e2tv+3trb/t7a297e2try3trZat7a2DAAAAADfti8/37Yv9t+2 + L//fti//3rQt/8aIDv/YqCX/37Yv/9GcHP/Qmhr/37Yv/9+2L//fti/637YvHAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC3trYHt7a2Obe2 + tqS3trbtt7a2/7e2tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2tv63trbqt7a2lre2tjO3trYEAAAAAAAA + AADfti8W37Yv1d+2L//fti//37Yv/9uwKv/Qmxv/37Yv/9+2L//Zqyf/1aMh/9+2L//fti//37Yv/9+2 + L2MAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALe2 + tgy3trZlt7a2xre2tv23trb/t7a2/7e2tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2ttK3trZqt7a2FgAA + AAAAAAAAAAAAAAAAAADfti8C37Yvo9+2L//fti//37Yv/9+2L//brin23rQu/9+2L//fti//3rUu/duv + KvDfti//37Yv/9+2L//fti+p37YvAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAC3trYBt7a2L7e2to+3trbut7a2/7e2tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2tv+3trbyt7a2pre2 + tkC3trYBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA37YvYt+2L/zfti//37Yv/9+2L//fti/p37YvrN+2 + L//fti//37Yv/9+2L/Dfti+m37Yv/9+2L//fti//37Yv6d+2Lw8AAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAALe2tg63trZSt7a2u7e2tva3trb/t7a2/7e2tv+3trb/t7a2/7e2tv+3trb/t7a2/re2 + ttq3trZ8t7a2ILe2tgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA37YvKN+2L+/fti//37Yv/9+2 + L//fti/837YvYN+2L7Xfti//37Yv/9+2L//fti/G37YvV9+2L/7fti//37Yv/9+2L//fti9DAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAC3trYCt7a2H7e2tn63trbZt7a2/re2tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2 + tv+3trb3t7a2ure2tlO3trYNAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA37YvDN+2 + L8Lfti//37Yv/9+2L//fti//37Yvod+2LxHfti/b37Yv/9+2L//fti//37YvlN+2Lxbfti/037Yv/9+2 + L//fti//37YvjQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAALe2tgK3trZBt7a2pre2tvS3trb/t7a2/7e2tv+3trb/t7a2/7e2 + tv+3trb/t7a2/7e2tuy3traPt7a2Lre2tgEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAN+2L4Xfti//37Yv/9+2L//fti//37Yv3d+2LxDfti8i37Yv9d+2L//fti//37Yv/9+2 + L2QAAAAA37Yvv9+2L//fti//37Yv/9+2L9Dfti8HAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAt7a2Fre2tmy3trbUt7a2/re2tv+3trb/t7a2/7e2 + tv+3trb/t7a2/7e2tv+3trb9t7a2xbe2tmO3trYNAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAN+2L0rfti/337Yv/9+2L//fti//37Yv9N+2Lz4AAAAA37YvS961 + L//asi7/1q8t/9KsLv3Esndyt7a2X7+oXrqwkyf/YWcl/zpSI/91dyf4sp0sKwAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALe2tgS3trY0t7a2lre2tum3trb/t7a2/7e2 + tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2tu23traht7a2O7e2tgcAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAt7a2CMasViTXry3f1a4t/9CqLP/KpSr/xKAq/8Ot + aNu3traft7a2tq+dZN+niCP/pYYj/6OFI/+jiC//tbGp/7e2tv+XmJf/LEAj/wYsHv8GLB7/By0e/yhO + On8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAt7a2Dbe2tli3tra/t7a2+Le2 + tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2tv+3trb8t7a22re2tnW3trYeAAAAAAAAAAAAAAAAAAAAALe2 + tgK3trYFt7a2Cbe2tg23trYbt7a2Nbe2tky3trZlt7a2fLe2tpS3tratt7a2xLe1s9Spk0vspIUj/6CC + Iv+ggiL/oIIi/6aQSv+2tLD/t7a2/7e2tv+ok1P/oIIi/6CCIv+ggiL/pIw+/7W0tf+AjLD/MlCk/wow + M/8GLB7/Biwe/wYsHv8eRz+4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC3trYjt7a2hLe2 + tuG3trb/t7a2/7e2tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2tvu3trawt7a2Ure2tiC3trYvt7a2Rbe2 + tlq3trZ0t7a2i7e2tqK3tra6t7a2zbe2tuO3trb2t7a2/7e2tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2 + tv+uoXr/oIIi/6CCIv+ggiL/oIIi/6KHL/+0r6L/t7a2/7e2tv+3tbT/o4k2/6CCIv+ggiL/oIIi/56O + Wv9dcaz/LUyn/x5Gpv8GNGD/Biwe/wYsHv8GLSD/G0xIcgAAAAAAAAAAAAAAAAAAAAAAAAAAt7a2Cbe2 + tkm3travt7a297e2tv+0s7P/srGx/66trf+rqqr/p6am/6Khof+cm5v/np2d/6uqqv60s7Pht7a25Le2 + tvW3trb4t7a2+7e2tv23trb/t7a2/7e2tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2 + tv+3trb/t7a2/7e2tv+yrJj/oYUr/6CCIv+ggiL/oIIi/6CCI/+vpYX/t7a2/7e2tv+3trb/tLCk/6GF + Kv+ggiL/oIIi/4JvIf88Tm7/LUyn/y5Oq/8PQ6z/BkGr/wc7eP8IQGD/F4Sd4ym42xYAAAAAAAAAAAAA + AACTg5Ygg3SGgJCJkdOZl5j7lZSU/5CPj/+Lior/iYiI/4eGhv+FhIT/g4KC/4SDg/+Pjo7/pKOj/7Oy + sv+3trb/t7a2/7e2tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2 + tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2tv+2s63/pY1C/6CCIv+ggiL/oIIi/6CCIv+pllz6t7a18be2 + tui3trbgt7a2zberh825lyj/uZgo/3lxKP83SCf/OV2z/zxm4P8xY+L/ClXi/wlV5P8ObOr/ILf3/ybP + +5EAAAAAAAAAAAAAAACLeo4acmB23mlWbf9pVm3/cmV0/4KAgf+DgoL/g4KC/4OCgv+DgoL/hoWF/5iX + l/+sq6v/trW1/7e2tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2 + tv+3trb/t7a2/7e2tv+3trb/t7a2/re2tvy3trb6t7a2+Le2tuy3trbWuKNi9baVJ/+8mSj/wp4p/8ij + Kv/KpSzUtaqGN7e2tiO3trYTt7a2BAAAAADfti90zasv/2psLP84TCv/N042/z5n2P8/aur/H17o/wlY + 5v8Uhe7/JMf6/ybQ+/wm0PsvAAAAAAAAAAAAAAAAjHqPcWlWbf9pVm3/aVZt/2lWbf9wYnP/gX+B/4OC + gv+Mi4v/oJ+f/7Oysv+3trb/t7a2/7e2tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2tv+3trb/t7a2/7e2 + tv+3trb9t7a27Le2tta3trbDt7a2rre2tpa3trZ+t7a2Zre2tk+3trY4t7a2I7e2tg4AAAAA37YvTt+2 + L/zfti//37Yv/9+2L//fti/y37YvNwAAAAAAAAAAAAAAAAAAAAAAAAAAlIczp0pYK/83TCv/N0wr/zhR + Tv8/aeT/O2nq/w9g6P8cpPT/Js/7/ybQ+/8m0Pu9JtD7BAAAAAAAAAAAAAAAAI59kVVrWG/+aVZt/2lW + bf9pVm3/a1hv/31sgP+moqX/tbS0/7e2tv+3trb/t7a2/7e2tve3trbot7a227e2ts63tra3t7a2obe2 + toe3trZvt7a2Wbe2tkG3trYmt7a2Ebe2tgy3trYHt7a2AwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAA37YvH9+2L+Pfti//37Yv/9+2L//fti/+37YvcN+2LwEAAAAAAAAAAAAAAAA/auoMP2rqgzlR + SPs3TCv/N0wr/zdMK/86V27/P2rq/y567PchvPj/JtD7/ybQ+/8m0Pv+JtD7WQAAAAAAAAAAAAAAAAAA + AACjk6UJfmqBoHFddvx0X3n/hm+M/5p/of+mia3/s5q5/ruyvZu3trZht7a2Sre2tjG3trYit7a2F7e2 + tg23trYDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAA37YvCN+2L7Lfti//37Yv/9+2L//fti//37Yvst+2LwcAAAAAAAAAAAAA + AAA/auonP2rqvD9p4f43TC7/N0wr/zdMK/83TCv/O1yQ+D5v65YnyfrTJtD7/ybQ+/8m0Pv/JtD76CbQ + +wsAAAAAAAAAAAAAAAAAAAAAAAAAAM2q1gPCocuAyafS/c2q1v/Nqtb/zarW/82q1v/NqtbXzarWKwAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAN+2L3Lfti/937Yv/9+2L//fti//37Yv5t+2 + LxwAAAAAAAAAAD9q6gE/aupSP2rq5z9q6v89ZMH/N0wr/zdMK/83TCv/RFQr/2Nxbmonzfs8JtD7+ybQ + +/8m0Pv/JtD7/ybQ+40AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM2q1m7Nqtb4zarW/82q + 1v/Nqtb/zarW/82q1ubNqtY2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAN+2Lznfti/z37Yv/9+2 + L//fti//37Yv+N+2L00AAAAAAAAAAD9q6g4/auqSP2rq/z9q6v8/aur/PF2Z/zdMK/83TCv/ZGgs/8uq + L/7fti8SJtD7oCbQ+/8m0Pv/JtD7/ybQ+/Qm0PstAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AADNqtYCzarWXc2q1vTNqtb/zarW/82q1v/Nqtb/zarW682q1kMAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAN+2 + Lw3fti/W37Yv/9+2L//fti//37Yv/9+2L43fti8CAAAAAD9q6i8/aurKP2rq/j9q6v8/aur/P2rq/zpY + d/89UCv/jIIt/9myL//fti/cQMzeHibQ++sm0Pv/JtD7/ybQ+/8m0Pu6JtD7AQAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAM2q1gHNqtZNzarW8s2q1v/Nqtb/zarW/82q1v/NqtbwzarWUM2q + 1gEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAN+2LwTfti+Y37Yv/9+2L//fti//37Yv/9+2L8nfti8PP2rqBD9q6mI/aursP2rq/z9q + 6v8/aur/P2rq/z9p5v9NX1b/spou/961L//fti//37YvrifQ+m8m0Pv/JtD7/ybQ+/8m0Pv+JtD7VgAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM2q1kDNqtbtzarW/82q + 1v/Nqtb/zarW/82q1vfNqtZeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADfti9V37Yv+9+2L//fti//37Yv/9+2L/Lfti8sP2rqET9q + 6p4/aur+P2rq/z9q6v8/aur/P2rq/z9q6v9Lb9m+zaw299+2L//fti//37Yv/9y2MoEm0PvUJtD7/ybQ + +/8m0Pv/JtD73ybQ+xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAzarWNM2q1t7Nqtb/zarW/82q1v/Nqtb/zarW/M2q1nHNqtYCAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADfti8m37Yv6N+2L//fti//37Yv/9+2 + L/3fti9oQGrpOj9q6tU/aur/P2rq/z9q6v8/aur/P2rq/z9q6vI/aupv0rA+Mt+2L//fti//37Yv/9+2 + L/6LwouOJtD7/SbQ+/8m0Pv/JtD7/ybQ+4UAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADNqtYtzarW1c2q1v/Nqtb/zarW/82q1v/Nqtb6zarWg82q + 1gUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADfti8J37Yvvd+2 + L//fti//37Yv/9+2L//ZszawRGzkdD9q6vM/aur/P2rq/z9q6v8/aur/P2rq/z9q6tQ/auo5P2rqAd+2 + L2Lfti//37Yv/9+2L//etjD3RsvWxCbQ+/8m0Pv/JtD7/ybQ+/km0PsjAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM2q1iDNqtbQzarW/82q + 1v/Nqtb/zarW/82q1vzNqtaRzarWBgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAA37Yve9+2L/3fti//37Yv/9+2L//NrT/wVHTRxT9q6v4/aur/P2rq/z9q6v8/aur/P2rq/D9q + 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 + Lv8hlC7/JcPP/ybQ++wm0PsgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AADfti94wrEv/ymVLv8hlC7/IZUx/ybO9P8m0PukJtD7AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAA37Yvp3ukLv8hlC7/IZQu/yKfUv8m0Pr+JtD7PwAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA37YvCNe1L9Ezly7/IZQu/yGULv8jqnr/JtD7zibQ + +wkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAN+2Lxmdqi/0IZQu/yGU + Lv8hlC7/JLWg/ybQ+20AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AADUtC87V54u/yGULv8hlC7/IZUw/yXAw/Qm0PsRAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAk6kvbyeVLv8hlC7/IZQu/yGXOf8lxdegAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEWeOqshlC7/IZQu/yGULv8inEj6Js/4PQAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAznEDQIZQu/yGU + Lv8hlC7/J6BT0CbQ+wQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAOaBHnCGULv8hlC7/IpQv/zmhTHEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAFKrYQ44n0aRI5UwtT2hS35suH0JAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/////////////////////////////8f/// + //////g/////////wB////////4AH///////+AAf///////gAB///////wAAD//////8AAAP/////+AA + AA//////gAAAD/////4AAgAH////8AAMAAf////AAHgAA////gAB+AAD///4AAfwAAP//8AAP+AAA/// + AAD/4ACB//wAB//AQAH/4AAf/wAAAf+AAPAAAAAB/gAAAAAAAAHwAAAAAAAAAcAAAAAAAAADgAAAAAAA + gAOAAAAACA+AA4AAAA/wDgAHgAB//+AcAAfAH///4DAAD/AP///AYAAP8Af//4BAAA/4Af//AAAAH/4B + //8AAAAf/wB//gAAAD//gD/8AAAAP//AH/wABgA//+AP+AAOAH//8AfwADwAf//4A+AAfAD///wB4AD8 + AP///gDAA/wB////AAAH/AH///+AAA/4Af///+AAP/gD////8AB/+AP////4AP/4A/////wD//AH//// + /gf/8Af/////H//wD/////////AP////////8A/////////wH////////+Af////////4D/////////g + P////////+B/////////4H/////////gf////////+D/////////4P////////////////////////// + ////////iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAAgAElEQVR42u29eZRk133f97n3 + bbV19To9K2YADDAABrsAgYsIQhRNygSphZJNndhiZFG2adOREx2dJMfSsSImIn0sKUpkRfQRHZ1IpkxZ + NBcllEGJMSlCYkhQIIhlsGOAGcz0LD29d+1vub/88bp7umd6qa6uXqrqfs9pDKan3qt6r97ve7/f3/3d + 3wULCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLC4uOgLK3wKLT + 8S8e+6hcKF9iqjbD+dIYAFppsm6WG4qHGc4M8W8/+Mf2WV8F2t4Ci07H67NnmaxNU4kqS78TEWITM1uf + 41LlMh/8o3eLvVNWAVh0ET702R+WWlznzflz649yC2rgrpGT/Luf/Jx95i0BWHQ6fuqz75Op+hRTtWkS + SZp40BU5L0efX+CrP/uEfe6tBbDoZEzXp6lE1aaCH0AQwiSkkTT4+f/7p60dsARg0an4n7/638lUfWqF + 528GkYmITcx8WLI30RKARadirHyxZfdaCss8M37K3kRLABadilpcR6R1FS9YB2AJwKJj0YjDLZ/jN/7s + EcsClgAsehVxPMW/e+wesQRgYdFhyHqZLZ9jLkxnBCwBWFh0GHw3j1Jbm8qfjxLiJLYEYB8ni05D5J1E + tljDdr4u1AxEz5wQSwAWFh2EP/jALyuji6BatwIToVBN7L107S2w6ESIsw9QENdbOr6aQN3Q8yRgFYBF + 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 + GBgsBv8PNAlKsBafvBgAAAAASUVORK5CYII= + + + \ No newline at end of file diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs b/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs new file mode 100644 index 00000000..2503a1a7 --- /dev/null +++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs @@ -0,0 +1,54 @@ +using System; +using System.Linq; +using System.Drawing.Design; +using System.ComponentModel; +using System.Windows.Forms.Design; +using System.Windows.Forms; +using System.Reactive.Linq; +using System.Numerics; +using Bonsai.Design; + + +namespace OpenEphys.Onix1.Design +{ + /// + /// Provides a user interface editor that displays a dialog for selecting + /// members of a workflow expression type. + /// + public class SpatialTransformMatrixEditor : DataSourceTypeEditor + { + public SpatialTransformMatrixEditor() + : base(DataSource.Input, typeof(void)) + { + } + + /// + public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context) + { + return UITypeEditorEditStyle.Modal; + } + + protected virtual IObservable> GetData(IObservable> source) + { + return source.Merge().Select(coordinate => (Tuple)coordinate); + } + + public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value) + { + var editorService = (IWindowsFormsEditorService)provider.GetService(typeof(IWindowsFormsEditorService)); + if (context != null && editorService != null) + { + var source = GetDataSource(context, provider); + var dataFrames = GetData(source.Output); + using (var visualizerDialog = new SpatialTransformMatrixDialog(dataFrames)) + { + if (editorService.ShowDialog(visualizerDialog) == DialogResult.OK && visualizerDialog.ApplySpatialTransform) + { + return visualizerDialog.SpatialTransform; + } + } + } + return base.EditValue(context, provider, value); + } + } +} diff --git a/OpenEphys.Onix1/SpatialTransform.cs b/OpenEphys.Onix1/SpatialTransform.cs new file mode 100644 index 00000000..49dab035 --- /dev/null +++ b/OpenEphys.Onix1/SpatialTransform.cs @@ -0,0 +1,36 @@ +using Bonsai; +using System; +using System.ComponentModel; +using System.Linq; +using System.Reactive.Linq; +using System.Numerics; + +namespace OpenEphys.Onix1 +{ + /// + /// Represents an operator that groups the elements of an observable + /// sequence according to the specified key. + /// + [DefaultProperty(nameof(SpatialTransformMatrix))] + [Description("Transforms 3D coordinates from one reference frame to another.")] + public class SpatialTransform : Transform, Vector3> + { + /// + /// Gets or sets a value specifying the inner properties used as key for + /// each element in the sequence. + /// + [Description("Spatial Transform Matrix")] + [Editor("OpenEphys.Onix1.Design.SpatialTransformMatrixEditor, OpenEphys.Onix1.Design", DesignTypes.UITypeEditor)] + [TypeConverter(typeof(NumericRecordConverter))] + public Matrix4x4 SpatialTransformMatrix { get; set; } = new Matrix4x4( + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1); + + public override IObservable Process(IObservable> source) + { + return source.Select(input => Vector3.Transform(input.Item2, this.SpatialTransformMatrix)); + } + } +} From 39c344ec1e90aeea244884daa6161abeaa44fafb Mon Sep 17 00:00:00 2001 From: cjsha Date: Mon, 18 Aug 2025 16:08:06 -0400 Subject: [PATCH 14/17] Address jpn feedback - Add cancel button - Add timeout (probably only need timeout or cancel button) - Check workflow is running before opening GUI - Add status strip - Use "OK", "Cancel" button paradigms Also: - Give persistent scope to subscriptions so they can be disposed - Simplify conditional statement for checking if user input is valid - Minor edit to top-level label to be consistent with changes - Improve resizing - PascalCase methods - Make "using" syntax more concise - Add/Edit some XML comments - Instead of transforming every Vector3 and then averaging, average all Vector3s and then take the transform - Inline/remove the GetData() function --- .../SpatialTransformMatrixDialog.Designer.cs | 186 ++++++----- .../SpatialTransformMatrixDialog.cs | 290 +++++++++++++----- .../SpatialTransformMatrixDialog.resx | 19 +- .../SpatialTransformMatrixEditor.cs | 28 +- OpenEphys.Onix1/SpatialTransform.cs | 36 --- OpenEphys.Onix1/SpatialTransform3D.cs | 79 +++++ OpenEphys.Onix1/TS4231V1SpatialTransform.cs | 40 +++ 7 files changed, 466 insertions(+), 212 deletions(-) delete mode 100644 OpenEphys.Onix1/SpatialTransform.cs create mode 100644 OpenEphys.Onix1/SpatialTransform3D.cs create mode 100644 OpenEphys.Onix1/TS4231V1SpatialTransform.cs diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.Designer.cs b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.Designer.cs index eea32388..17e2994e 100644 --- a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.Designer.cs +++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.Designer.cs @@ -56,18 +56,22 @@ private void InitializeComponent() this.labelHeaderUser = new System.Windows.Forms.Label(); this.buttonCalculate = new System.Windows.Forms.Button(); this.flowLayoutPanelBottom = new System.Windows.Forms.FlowLayoutPanel(); - this.buttonClose = new System.Windows.Forms.Button(); - this.checkBoxApplySpatialTransform = new System.Windows.Forms.CheckBox(); + this.buttonCancel = new System.Windows.Forms.Button(); + this.buttonOK = new System.Windows.Forms.Button(); + this.statusStrip = new System.Windows.Forms.StatusStrip(); + this.toolStripStatusLabelTS4231 = new System.Windows.Forms.ToolStripStatusLabel(); + this.toolStripStatusLabelUser = new System.Windows.Forms.ToolStripStatusLabel(); this.tableLayoutPanelMain.SuspendLayout(); this.groupBoxStatus.SuspendLayout(); this.tableLayoutPanelCoordinates.SuspendLayout(); this.flowLayoutPanelBottom.SuspendLayout(); + this.statusStrip.SuspendLayout(); this.SuspendLayout(); // // tableLayoutPanelMain // this.tableLayoutPanelMain.ColumnCount = 1; - this.tableLayoutPanelMain.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); + this.tableLayoutPanelMain.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); this.tableLayoutPanelMain.Controls.Add(this.groupBoxStatus, 0, 2); this.tableLayoutPanelMain.Controls.Add(this.labelInstructions, 0, 0); this.tableLayoutPanelMain.Controls.Add(this.tableLayoutPanelCoordinates, 0, 1); @@ -82,7 +86,7 @@ private void InitializeComponent() this.tableLayoutPanelMain.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F)); this.tableLayoutPanelMain.RowStyles.Add(new System.Windows.Forms.RowStyle()); this.tableLayoutPanelMain.RowStyles.Add(new System.Windows.Forms.RowStyle()); - this.tableLayoutPanelMain.Size = new System.Drawing.Size(624, 661); + this.tableLayoutPanelMain.Size = new System.Drawing.Size(604, 639); this.tableLayoutPanelMain.TabIndex = 7; // // groupBoxStatus @@ -91,7 +95,7 @@ private void InitializeComponent() this.groupBoxStatus.Dock = System.Windows.Forms.DockStyle.Fill; this.groupBoxStatus.Location = new System.Drawing.Point(3, 263); this.groupBoxStatus.Name = "groupBoxStatus"; - this.groupBoxStatus.Size = new System.Drawing.Size(618, 330); + this.groupBoxStatus.Size = new System.Drawing.Size(598, 308); this.groupBoxStatus.TabIndex = 6; this.groupBoxStatus.TabStop = false; this.groupBoxStatus.Text = "Status Messages"; @@ -106,23 +110,23 @@ private void InitializeComponent() this.textBoxStatus.Name = "textBoxStatus"; this.textBoxStatus.ReadOnly = true; this.textBoxStatus.ScrollBars = System.Windows.Forms.ScrollBars.Vertical; - this.textBoxStatus.Size = new System.Drawing.Size(612, 311); + this.textBoxStatus.Size = new System.Drawing.Size(592, 289); this.textBoxStatus.TabIndex = 3; this.textBoxStatus.Text = "Awaiting user input...\r\n"; // // labelInstructions // this.labelInstructions.AutoSize = true; - this.labelInstructions.Dock = System.Windows.Forms.DockStyle.Fill; this.labelInstructions.Location = new System.Drawing.Point(3, 0); - this.labelInstructions.MaximumSize = new System.Drawing.Size(620, 0); this.labelInstructions.Name = "labelInstructions"; - this.labelInstructions.Size = new System.Drawing.Size(618, 104); + this.labelInstructions.Size = new System.Drawing.Size(596, 104); this.labelInstructions.TabIndex = 4; this.labelInstructions.Text = resources.GetString("labelInstructions.Text"); // // tableLayoutPanelCoordinates // + this.tableLayoutPanelCoordinates.AutoSize = true; + this.tableLayoutPanelCoordinates.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; this.tableLayoutPanelCoordinates.ColumnCount = 4; this.tableLayoutPanelCoordinates.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); this.tableLayoutPanelCoordinates.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); @@ -155,57 +159,53 @@ private void InitializeComponent() this.tableLayoutPanelCoordinates.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 30F)); this.tableLayoutPanelCoordinates.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 30F)); this.tableLayoutPanelCoordinates.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 30F)); - this.tableLayoutPanelCoordinates.Size = new System.Drawing.Size(618, 150); + this.tableLayoutPanelCoordinates.Size = new System.Drawing.Size(598, 150); this.tableLayoutPanelCoordinates.TabIndex = 5; this.tableLayoutPanelCoordinates.Tag = "6"; // // textBoxUserCoordinate3 // this.textBoxUserCoordinate3.Dock = System.Windows.Forms.DockStyle.Fill; - this.textBoxUserCoordinate3.Location = new System.Drawing.Point(395, 123); + this.textBoxUserCoordinate3.Location = new System.Drawing.Point(385, 123); this.textBoxUserCoordinate3.MinimumSize = new System.Drawing.Size(150, 4); this.textBoxUserCoordinate3.Name = "textBoxUserCoordinate3"; - this.textBoxUserCoordinate3.Size = new System.Drawing.Size(220, 20); + this.textBoxUserCoordinate3.Size = new System.Drawing.Size(210, 20); this.textBoxUserCoordinate3.TabIndex = 39; this.textBoxUserCoordinate3.Tag = "7"; - this.textBoxUserCoordinate3.Text = "x\'3, y\'3, z\'3"; - this.textBoxUserCoordinate3.TextChanged += new System.EventHandler(this.textBoxUserCoordinate_TextChanged); + this.textBoxUserCoordinate3.TextChanged += new System.EventHandler(this.TextBoxUserCoordinate_TextChanged); // // textBoxUserCoordinate2 // this.textBoxUserCoordinate2.Dock = System.Windows.Forms.DockStyle.Fill; - this.textBoxUserCoordinate2.Location = new System.Drawing.Point(395, 93); + this.textBoxUserCoordinate2.Location = new System.Drawing.Point(385, 93); this.textBoxUserCoordinate2.MinimumSize = new System.Drawing.Size(150, 4); this.textBoxUserCoordinate2.Name = "textBoxUserCoordinate2"; - this.textBoxUserCoordinate2.Size = new System.Drawing.Size(220, 20); + this.textBoxUserCoordinate2.Size = new System.Drawing.Size(210, 20); this.textBoxUserCoordinate2.TabIndex = 38; this.textBoxUserCoordinate2.Tag = "6"; - this.textBoxUserCoordinate2.Text = "x\'2, y\'2, z\'2"; - this.textBoxUserCoordinate2.TextChanged += new System.EventHandler(this.textBoxUserCoordinate_TextChanged); + this.textBoxUserCoordinate2.TextChanged += new System.EventHandler(this.TextBoxUserCoordinate_TextChanged); // // textBoxUserCoordinate1 // this.textBoxUserCoordinate1.Dock = System.Windows.Forms.DockStyle.Fill; - this.textBoxUserCoordinate1.Location = new System.Drawing.Point(395, 63); + this.textBoxUserCoordinate1.Location = new System.Drawing.Point(385, 63); this.textBoxUserCoordinate1.MinimumSize = new System.Drawing.Size(150, 4); this.textBoxUserCoordinate1.Name = "textBoxUserCoordinate1"; - this.textBoxUserCoordinate1.Size = new System.Drawing.Size(220, 20); + this.textBoxUserCoordinate1.Size = new System.Drawing.Size(210, 20); this.textBoxUserCoordinate1.TabIndex = 37; this.textBoxUserCoordinate1.Tag = "5"; - this.textBoxUserCoordinate1.Text = "x\'1, y\'1, z\'1"; - this.textBoxUserCoordinate1.TextChanged += new System.EventHandler(this.textBoxUserCoordinate_TextChanged); + this.textBoxUserCoordinate1.TextChanged += new System.EventHandler(this.TextBoxUserCoordinate_TextChanged); // // textBoxUserCoordinate0 // this.textBoxUserCoordinate0.Dock = System.Windows.Forms.DockStyle.Fill; - this.textBoxUserCoordinate0.Location = new System.Drawing.Point(395, 33); + this.textBoxUserCoordinate0.Location = new System.Drawing.Point(385, 33); this.textBoxUserCoordinate0.MinimumSize = new System.Drawing.Size(150, 4); this.textBoxUserCoordinate0.Name = "textBoxUserCoordinate0"; - this.textBoxUserCoordinate0.Size = new System.Drawing.Size(220, 20); + this.textBoxUserCoordinate0.Size = new System.Drawing.Size(210, 20); this.textBoxUserCoordinate0.TabIndex = 36; this.textBoxUserCoordinate0.Tag = "4"; - this.textBoxUserCoordinate0.Text = "x\'0, y\'0, z\'0"; - this.textBoxUserCoordinate0.TextChanged += new System.EventHandler(this.textBoxUserCoordinate_TextChanged); + this.textBoxUserCoordinate0.TextChanged += new System.EventHandler(this.TextBoxUserCoordinate_TextChanged); // // textBoxTS4231Coordinate3 // @@ -214,12 +214,10 @@ private void InitializeComponent() this.textBoxTS4231Coordinate3.MinimumSize = new System.Drawing.Size(150, 4); this.textBoxTS4231Coordinate3.Name = "textBoxTS4231Coordinate3"; this.textBoxTS4231Coordinate3.ReadOnly = true; - this.textBoxTS4231Coordinate3.Size = new System.Drawing.Size(220, 20); + this.textBoxTS4231Coordinate3.Size = new System.Drawing.Size(210, 20); this.textBoxTS4231Coordinate3.TabIndex = 33; this.textBoxTS4231Coordinate3.TabStop = false; this.textBoxTS4231Coordinate3.Tag = "3"; - this.textBoxTS4231Coordinate3.Text = "x3, y3, z3"; - this.textBoxTS4231Coordinate3.TextChanged += new System.EventHandler(this.textBoxTS4231Coordinate_TextChanged); // // textBoxTS4231Coordinate2 // @@ -228,12 +226,10 @@ private void InitializeComponent() this.textBoxTS4231Coordinate2.MinimumSize = new System.Drawing.Size(150, 4); this.textBoxTS4231Coordinate2.Name = "textBoxTS4231Coordinate2"; this.textBoxTS4231Coordinate2.ReadOnly = true; - this.textBoxTS4231Coordinate2.Size = new System.Drawing.Size(220, 20); + this.textBoxTS4231Coordinate2.Size = new System.Drawing.Size(210, 20); this.textBoxTS4231Coordinate2.TabIndex = 32; this.textBoxTS4231Coordinate2.TabStop = false; this.textBoxTS4231Coordinate2.Tag = "2"; - this.textBoxTS4231Coordinate2.Text = "x2, y2, z2"; - this.textBoxTS4231Coordinate2.TextChanged += new System.EventHandler(this.textBoxTS4231Coordinate_TextChanged); // // textBoxTS4231Coordinate1 // @@ -242,12 +238,10 @@ private void InitializeComponent() this.textBoxTS4231Coordinate1.MinimumSize = new System.Drawing.Size(150, 4); this.textBoxTS4231Coordinate1.Name = "textBoxTS4231Coordinate1"; this.textBoxTS4231Coordinate1.ReadOnly = true; - this.textBoxTS4231Coordinate1.Size = new System.Drawing.Size(220, 20); + this.textBoxTS4231Coordinate1.Size = new System.Drawing.Size(210, 20); this.textBoxTS4231Coordinate1.TabIndex = 31; this.textBoxTS4231Coordinate1.TabStop = false; this.textBoxTS4231Coordinate1.Tag = "1"; - this.textBoxTS4231Coordinate1.Text = "x1, y1, z1"; - this.textBoxTS4231Coordinate1.TextChanged += new System.EventHandler(this.textBoxTS4231Coordinate_TextChanged); // // buttonMeasure3 // @@ -258,7 +252,7 @@ private void InitializeComponent() this.buttonMeasure3.Tag = "3"; this.buttonMeasure3.Text = "Measure"; this.buttonMeasure3.UseVisualStyleBackColor = true; - this.buttonMeasure3.Click += new System.EventHandler(this.buttonMeasure_Click); + this.buttonMeasure3.Click += new System.EventHandler(this.ButtonMeasure_Click); // // buttonMeasure2 // @@ -269,7 +263,7 @@ private void InitializeComponent() this.buttonMeasure2.Tag = "2"; this.buttonMeasure2.Text = "Measure"; this.buttonMeasure2.UseVisualStyleBackColor = true; - this.buttonMeasure2.Click += new System.EventHandler(this.buttonMeasure_Click); + this.buttonMeasure2.Click += new System.EventHandler(this.ButtonMeasure_Click); // // buttonMeasure1 // @@ -281,7 +275,7 @@ private void InitializeComponent() this.buttonMeasure1.Tag = "1"; this.buttonMeasure1.Text = "Measure"; this.buttonMeasure1.UseVisualStyleBackColor = true; - this.buttonMeasure1.Click += new System.EventHandler(this.buttonMeasure_Click); + this.buttonMeasure1.Click += new System.EventHandler(this.ButtonMeasure_Click); // // labelCoordinate3 // @@ -321,7 +315,7 @@ private void InitializeComponent() this.buttonMeasure0.Tag = "0"; this.buttonMeasure0.Text = "Measure"; this.buttonMeasure0.UseVisualStyleBackColor = true; - this.buttonMeasure0.Click += new System.EventHandler(this.buttonMeasure_Click); + this.buttonMeasure0.Click += new System.EventHandler(this.ButtonMeasure_Click); // // labelHeaderTS4231 // @@ -330,7 +324,7 @@ private void InitializeComponent() this.labelHeaderTS4231.Location = new System.Drawing.Point(80, 0); this.labelHeaderTS4231.Margin = new System.Windows.Forms.Padding(0); this.labelHeaderTS4231.Name = "labelHeaderTS4231"; - this.labelHeaderTS4231.Size = new System.Drawing.Size(312, 30); + this.labelHeaderTS4231.Size = new System.Drawing.Size(302, 30); this.labelHeaderTS4231.TabIndex = 0; this.labelHeaderTS4231.Text = "Naive TS4231 Coordinates"; this.labelHeaderTS4231.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; @@ -351,20 +345,18 @@ private void InitializeComponent() this.textBoxTS4231Coordinate0.MinimumSize = new System.Drawing.Size(150, 4); this.textBoxTS4231Coordinate0.Name = "textBoxTS4231Coordinate0"; this.textBoxTS4231Coordinate0.ReadOnly = true; - this.textBoxTS4231Coordinate0.Size = new System.Drawing.Size(220, 20); + this.textBoxTS4231Coordinate0.Size = new System.Drawing.Size(210, 20); this.textBoxTS4231Coordinate0.TabIndex = 30; this.textBoxTS4231Coordinate0.TabStop = false; this.textBoxTS4231Coordinate0.Tag = "0"; - this.textBoxTS4231Coordinate0.Text = "x0, y0, z0"; - this.textBoxTS4231Coordinate0.TextChanged += new System.EventHandler(this.textBoxTS4231Coordinate_TextChanged); // // labelHeaderUser // this.labelHeaderUser.Dock = System.Windows.Forms.DockStyle.Fill; - this.labelHeaderUser.Location = new System.Drawing.Point(395, 0); + this.labelHeaderUser.Location = new System.Drawing.Point(385, 0); this.labelHeaderUser.MinimumSize = new System.Drawing.Size(150, 0); this.labelHeaderUser.Name = "labelHeaderUser"; - this.labelHeaderUser.Size = new System.Drawing.Size(220, 30); + this.labelHeaderUser.Size = new System.Drawing.Size(210, 30); this.labelHeaderUser.TabIndex = 34; this.labelHeaderUser.Text = "User-Defined Coordinates"; this.labelHeaderUser.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; @@ -373,58 +365,87 @@ private void InitializeComponent() // this.buttonCalculate.Dock = System.Windows.Forms.DockStyle.Fill; this.buttonCalculate.Enabled = false; - this.buttonCalculate.Location = new System.Drawing.Point(3, 599); + this.buttonCalculate.Location = new System.Drawing.Point(3, 577); this.buttonCalculate.Name = "buttonCalculate"; - this.buttonCalculate.Size = new System.Drawing.Size(618, 23); + this.buttonCalculate.Size = new System.Drawing.Size(598, 23); this.buttonCalculate.TabIndex = 7; this.buttonCalculate.Text = "Calculate Spatial Transform"; this.buttonCalculate.UseVisualStyleBackColor = true; - this.buttonCalculate.Click += new System.EventHandler(this.buttonCalculate_Click); + this.buttonCalculate.Click += new System.EventHandler(this.ButtonCalculate_Click); // // flowLayoutPanelBottom // this.flowLayoutPanelBottom.AutoSize = true; - this.flowLayoutPanelBottom.Controls.Add(this.buttonClose); - this.flowLayoutPanelBottom.Controls.Add(this.checkBoxApplySpatialTransform); + this.flowLayoutPanelBottom.Controls.Add(this.buttonCancel); + this.flowLayoutPanelBottom.Controls.Add(this.buttonOK); this.flowLayoutPanelBottom.Dock = System.Windows.Forms.DockStyle.Fill; this.flowLayoutPanelBottom.FlowDirection = System.Windows.Forms.FlowDirection.RightToLeft; - this.flowLayoutPanelBottom.Location = new System.Drawing.Point(3, 628); + this.flowLayoutPanelBottom.Location = new System.Drawing.Point(3, 606); this.flowLayoutPanelBottom.Name = "flowLayoutPanelBottom"; - this.flowLayoutPanelBottom.Size = new System.Drawing.Size(618, 30); + this.flowLayoutPanelBottom.Size = new System.Drawing.Size(598, 30); this.flowLayoutPanelBottom.TabIndex = 8; // - // buttonClose - // - this.buttonClose.DialogResult = System.Windows.Forms.DialogResult.OK; - this.buttonClose.Location = new System.Drawing.Point(535, 3); - this.buttonClose.Name = "buttonClose"; - this.buttonClose.Size = new System.Drawing.Size(80, 24); - this.buttonClose.TabIndex = 0; - this.buttonClose.Text = "Close"; - this.buttonClose.UseVisualStyleBackColor = true; - this.buttonClose.Click += new System.EventHandler(this.buttonClose_Click); - // - // checkBoxApplySpatialTransform - // - this.checkBoxApplySpatialTransform.AutoSize = true; - this.checkBoxApplySpatialTransform.Dock = System.Windows.Forms.DockStyle.Fill; - this.checkBoxApplySpatialTransform.Enabled = false; - this.checkBoxApplySpatialTransform.Location = new System.Drawing.Point(268, 3); - this.checkBoxApplySpatialTransform.Name = "checkBoxApplySpatialTransform"; - this.checkBoxApplySpatialTransform.Size = new System.Drawing.Size(261, 24); - this.checkBoxApplySpatialTransform.TabIndex = 1; - this.checkBoxApplySpatialTransform.Text = "Set SpatialTransformMatrix property when closing."; - this.checkBoxApplySpatialTransform.UseVisualStyleBackColor = true; - this.checkBoxApplySpatialTransform.CheckedChanged += new System.EventHandler(this.checkBoxApplySpatialTransform_CheckedChanged); + // buttonCancel + // + this.buttonCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel; + this.buttonCancel.Location = new System.Drawing.Point(515, 3); + this.buttonCancel.Name = "buttonCancel"; + this.buttonCancel.Size = new System.Drawing.Size(80, 24); + this.buttonCancel.TabIndex = 0; + this.buttonCancel.Text = "Cancel"; + this.buttonCancel.UseVisualStyleBackColor = true; + this.buttonCancel.Click += new System.EventHandler(this.ButtonOKOrCancel_Click); + // + // buttonOK + // + this.buttonOK.DialogResult = System.Windows.Forms.DialogResult.OK; + this.buttonOK.Enabled = false; + this.buttonOK.Location = new System.Drawing.Point(429, 3); + this.buttonOK.Name = "buttonOK"; + this.buttonOK.Size = new System.Drawing.Size(80, 24); + this.buttonOK.TabIndex = 2; + this.buttonOK.Text = "OK"; + this.buttonOK.UseVisualStyleBackColor = true; + this.buttonOK.Click += new System.EventHandler(this.ButtonOKOrCancel_Click); + // + // statusStrip + // + this.statusStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.toolStripStatusLabelTS4231, + this.toolStripStatusLabelUser}); + this.statusStrip.Location = new System.Drawing.Point(0, 639); + this.statusStrip.Name = "statusStrip"; + this.statusStrip.ShowItemToolTips = true; + this.statusStrip.Size = new System.Drawing.Size(604, 22); + this.statusStrip.TabIndex = 8; + this.statusStrip.Text = "statusStrip1"; + // + // toolStripStatusLabelTS4231 + // + this.toolStripStatusLabelTS4231.Image = global::OpenEphys.Onix1.Design.Properties.Resources.StatusBlockedImage; + this.toolStripStatusLabelTS4231.Name = "toolStripStatusLabelTS4231"; + this.toolStripStatusLabelTS4231.Size = new System.Drawing.Size(237, 17); + this.toolStripStatusLabelTS4231.Text = "At least one TS4231 coordinate is invalid."; + this.toolStripStatusLabelTS4231.ToolTipText = "All four TS4231 coordinates must be measured before the spatial transform matrix " + + "can be calculated."; + // + // toolStripStatusLabelUser + // + this.toolStripStatusLabelUser.Image = global::OpenEphys.Onix1.Design.Properties.Resources.StatusBlockedImage; + this.toolStripStatusLabelUser.Name = "toolStripStatusLabelUser"; + this.toolStripStatusLabelUser.Size = new System.Drawing.Size(267, 17); + this.toolStripStatusLabelUser.Text = "At least one user-defined coordinate is invalid."; + this.toolStripStatusLabelUser.ToolTipText = resources.GetString("toolStripStatusLabelUser.ToolTipText"); // // SpatialTransformMatrixDialog // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(624, 661); + this.ClientSize = new System.Drawing.Size(604, 661); this.Controls.Add(this.tableLayoutPanelMain); + this.Controls.Add(this.statusStrip); this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); - this.MinimumSize = new System.Drawing.Size(640, 700); + this.MinimumSize = new System.Drawing.Size(620, 700); this.Name = "SpatialTransformMatrixDialog"; this.Text = "TS4231V1 Calibration GUI"; this.tableLayoutPanelMain.ResumeLayout(false); @@ -434,8 +455,10 @@ private void InitializeComponent() this.tableLayoutPanelCoordinates.ResumeLayout(false); this.tableLayoutPanelCoordinates.PerformLayout(); this.flowLayoutPanelBottom.ResumeLayout(false); - this.flowLayoutPanelBottom.PerformLayout(); + this.statusStrip.ResumeLayout(false); + this.statusStrip.PerformLayout(); this.ResumeLayout(false); + this.PerformLayout(); } @@ -465,8 +488,11 @@ private void InitializeComponent() private System.Windows.Forms.Label labelHeaderUser; private System.Windows.Forms.Button buttonCalculate; private System.Windows.Forms.FlowLayoutPanel flowLayoutPanelBottom; - private System.Windows.Forms.Button buttonClose; - private System.Windows.Forms.CheckBox checkBoxApplySpatialTransform; + private System.Windows.Forms.Button buttonCancel; private System.Windows.Forms.Label labelInstructions; + private System.Windows.Forms.Button buttonOK; + private System.Windows.Forms.StatusStrip statusStrip; + private System.Windows.Forms.ToolStripStatusLabel toolStripStatusLabelUser; + private System.Windows.Forms.ToolStripStatusLabel toolStripStatusLabelTS4231; } } diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs index 154c64c8..793375eb 100644 --- a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs +++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs @@ -7,101 +7,244 @@ namespace OpenEphys.Onix1.Design { + /// + /// Partial class to create a spatial-calibration GUI for . + /// public partial class SpatialTransformMatrixDialog : Form { - private const byte NumMeasurements = 100; + const byte NumMeasurements = 100; + readonly Matrix4x4 inverseM; - private bool[] InputsValid = { false, false, false, false, false, false, false, false }; + readonly bool[] InputsValid = { false, false, false, false, false, false, false, false }; + readonly IObservable PositionDataSource; + IDisposable richTextBoxStatusUpdateSubscription; + IDisposable MeasurementCalculationSubscription; - private IObservable> PositionDataSource; + internal Matrix4x4 NewSpatialTransform { get; private set; } - private Vector3[] TS4231Coordinates = { Vector3.Zero, Vector3.Zero, Vector3.Zero, Vector3.Zero }; + internal SpatialTransformMatrixDialog(IObservable positionDataSource, Matrix4x4 currentM) + { + InitializeComponent(); + SpatialTransform = transformProperties; + PositionDataSource = dataSource; + + var ts4231TextBoxes = new TextBox[] { + textBoxTS4231Coordinate0, textBoxTS4231Coordinate1, + textBoxTS4231Coordinate2, textBoxTS4231Coordinate3}; + foreach (var (textBox, v) in Enumerable.Zip(ts4231TextBoxes, SpatialTransform.Pre, (tb, v) => (tb, v))) + textBox.Text = checkVector3ForNaN(v) ? "" : $"{v.X}, {v.Y}, {v.Z}"; - internal Matrix4x4 SpatialTransform { get; private set; } + var userTextBoxes = new TextBox[] { + textBoxUserCoordinate0X, textBoxUserCoordinate0Y, textBoxUserCoordinate0Z, + textBoxUserCoordinate1X, textBoxUserCoordinate1Y, textBoxUserCoordinate1Z, + textBoxUserCoordinate2X, textBoxUserCoordinate2Y, textBoxUserCoordinate2Z, + textBoxUserCoordinate3X, textBoxUserCoordinate3Y, textBoxUserCoordinate3Z}; + for (byte i = 0; i < 12; i++) + { + ref var component = ref GetComponent(ref SpatialTransform.Post[i / 3], i % 3); + userTextBoxes[i].Text = float.IsNaN(component) ? "" : component.ToString(); + } - internal bool ApplySpatialTransform { get; private set; } + CalculatePrintMatrix(); + } - internal SpatialTransformMatrixDialog(IObservable> positionDataSource) + private void TextBoxUserCoordinate_TextChanged(object sender, EventArgs e) { - InitializeComponent(); - PositionDataSource = positionDataSource; + var tag = Convert.ToByte(((TextBox)sender).Tag); + ref var coordinateComponent = ref GetComponent(ref SpatialTransform.Post[tag / 3], tag % 3); + try { coordinateComponent = float.Parse(((TextBox)sender).Text); } + catch { coordinateComponent = float.NaN; } + CalculatePrintMatrix(); } - private void buttonMeasure_Click(object sender, EventArgs e) + private void ButtonMeasure_Click(object sender, EventArgs e) { TextBox[] ts4231TextBoxes = { textBoxTS4231Coordinate0, textBoxTS4231Coordinate1, textBoxTS4231Coordinate2, textBoxTS4231Coordinate3 }; - var index = int.Parse((string)((Button)sender).Tag); + var index = Convert.ToByte(((Button)sender).Tag); + ts4231TextBoxes[index].Text = ""; + SpatialTransform.Pre[index] = new(float.NaN); + if (((Button)sender).Text == "Measure") + { + richTextBoxStatus.SelectionColor = Color.Blue; + richTextBoxStatus.AppendText($"Measurement at coordinate {index} initiated.\n"); + SpatialTransform.M = null; + textBoxSpatialTransformMatrix.Text = ""; + ((Button)sender).Text = "Cancel"; + EnableButtons(false, index); - textBoxStatus.AppendText(string.Format("Measuring coordinate {0}...", index) + Environment.NewLine); + var sharedPositionDataGroups = PositionDataSource + .Take(NumMeasurements) + .Timeout(new TimeSpan(0, 0, 5), Observable.Empty()) + .Publish(); - buttonMeasure0.Enabled = false; - buttonMeasure1.Enabled = false; - buttonMeasure2.Enabled = false; - buttonMeasure3.Enabled = false; - buttonCalculate.Enabled = false; + TextBoxStatusUpdateSubscription = sharedPositionDataGroups + .GroupBy(dataFrame => dataFrame.SensorIndex, dataFrame => dataFrame.Position) + .SelectMany(group => group.Count().Select(count => new { Index = group.Key, MeasurementCount = count })) + .Aggregate( + (TextBoxStatusUpdate: "", Count: 0), + (acc, sensor) => + { + var textBoxStatusUpdateString = acc.TextBoxStatusUpdate; + textBoxStatusUpdateString += string.Format("{0} measurements from sensor {1}.", + sensor.MeasurementCount, sensor.Index); + textBoxStatusUpdateString += Environment.NewLine; + return (textBoxStatusUpdateString, acc.Count + sensor.MeasurementCount); + }, + acc => (acc.TextBoxStatusUpdate, Valid: acc.Count == NumMeasurements)) + .ObserveOn(new ControlScheduler(this)) + .Subscribe(finalResult => + { + if (finalResult.Valid) + { + textBoxStatus.AppendText(finalResult.TextBoxStatusUpdate); + textBoxStatus.AppendText(string.Format("Measurements at coordinate {0} complete.", index) + + Environment.NewLine + Environment.NewLine + "Awaiting user input..." + Environment.NewLine); + } + else + { + textBoxStatus.AppendText(string.Format("Measurements at coordinate {0} timed out. ", index) + + "Confirm the Lighthouse receivers are within range and unobstructed from Lighthouse transmitters." + + Environment.NewLine + Environment.NewLine + "Awaiting user input..." + Environment.NewLine); + } + EnableButtons(true, index); + }); - var sharedPositionDataGroups = PositionDataSource.Take(NumMeasurements) - .GroupBy(dataFrame => dataFrame.Item1, dataFrame => dataFrame.Item2) - .Publish(); + MeasurementCalculationSubscription = sharedPositionDataGroups + .Aggregate( + (Sum: Vector3.Zero, Count: 0), + (acc, current) => (acc.Sum + current.Position, acc.Count + 1), + acc => + { + SpatialTransform.Pre[index] = acc.Sum / NumMeasurements; + return (Position: SpatialTransform.Pre[index], Valid: acc.Count == NumMeasurements); + }) + .ObserveOn(new ControlScheduler(this)) + .Subscribe(finalMeasurement => + { + MeasureButtons[index].Text = "Measure"; + if (finalMeasurement.Valid) + { + ts4231TextBoxes[index].Text = string.Format("{0}, {1}, {2}", + finalMeasurement.Position.X, + finalMeasurement.Position.Y, + finalMeasurement.Position.Z); + InputsValid[index] = true; + if (InputsValid.Take(4).All(ts4231InputValid => ts4231InputValid)) + { + toolStripStatusLabelTS4231.Image = OpenEphys.Onix1.Design.Properties.Resources.StatusReadyImage; + toolStripStatusLabelTS4231.Text = "All TS4231 coordinates are valid."; + } + else + { + toolStripStatusLabelTS4231.Image = OpenEphys.Onix1.Design.Properties.Resources.StatusBlockedImage; + toolStripStatusLabelTS4231.Text = "At least one TS4231 coordinate is invalid."; + } + buttonCalculate.Enabled = InputsValid.All(inputValid => inputValid); + } + }); - sharedPositionDataGroups - .SelectMany(group => group.Count().Select(count => new { index = group.Key, measurementCount = count })) - .ObserveOn(new ControlScheduler(this)) - .Finally(() => - { - textBoxStatus.AppendText(string.Format("Measurements at coordinate {0} complete.", index) - + Environment.NewLine + Environment.NewLine + "Awaiting user input..." + Environment.NewLine); - buttonMeasure0.Enabled = true; - buttonMeasure1.Enabled = true; - buttonMeasure2.Enabled = true; - buttonMeasure3.Enabled = true; - buttonCalculate.Enabled = InputsValid.All(inputValid => inputValid); - }) - .Subscribe(sensor => + sharedPositionDataGroups.Connect(); + } + else + { + TextBoxStatusUpdateSubscription.Dispose(); + MeasurementCalculationSubscription.Dispose(); + textBoxStatus.AppendText(string.Format("Measurements at coordinate {0} cancelled by user.", index) + + Environment.NewLine + Environment.NewLine + "Awaiting user input..." + Environment.NewLine); + MeasureButtons[index].Text = "Measure"; + EnableButtons(true, index); + } + } + + private void ButtonOK_Click(object sender, EventArgs e) + { + if (SpatialTransform.M.HasValue) + DialogResult = DialogResult.OK; + else + { + var confirmationMessage = ""; + var incompleteInput = false; + if (SpatialTransform.Post.Any(userCoordinate => checkVector3ForNaN(userCoordinate))) { - textBoxStatus.AppendText(string.Format("{1} measurements from sensor {0}.", sensor.index, sensor.measurementCount) + Environment.NewLine); - }); - - sharedPositionDataGroups - .Merge() - .ObserveOn(new ControlScheduler(this)) - .Aggregate( - new Vector3(0, 0, 0), - (acc, current) => acc + current, - acc => + incompleteInput = true; + var axes = new char[] { 'X', 'Y', 'Z' }; + var coordinates = new byte[] { 0, 1, 2, 3 }; + confirmationMessage += "At least one coordinate component is empty or invalid:\n"; + for (byte i = 0; i < 12; i++) { - TS4231Coordinates[index] = acc / NumMeasurements; - ts4231TextBoxes[index].Text = string.Format("{0}, {1}, {2}", - TS4231Coordinates[index].X, - TS4231Coordinates[index].Y, - TS4231Coordinates[index].Z); - return TS4231Coordinates[index]; - }) - .Subscribe(); - - sharedPositionDataGroups.Connect(); - } - - private void textBoxTS4231Coordinate_TextChanged(object sender, EventArgs e) + ref var component = ref GetComponent(ref SpatialTransform.Post[i / 3], i % 3); + if (float.IsNaN(component)) + confirmationMessage += $" • Coordinate {coordinates[i / 3]} {axes[i % 3]} component\n"; + } + confirmationMessage += "\n"; + } + if (SpatialTransform.Pre.Any(TS4231Coordinate => checkVector3ForNaN(TS4231Coordinate))) + { + incompleteInput = true; + confirmationMessage += "At least one coordinate measurement is empty:\n"; + foreach (var (i, v) in SpatialTransform.Pre.Select((i, v) => (v, i))) + if (checkVector3ForNaN(v)) + confirmationMessage += $" • Coordinate {i}\n"; + confirmationMessage += "\n"; + } + + if (incompleteInput) + confirmationMessage += "They will not be saved and transformed position data won't be properly output.\n\n"; + else if (!Matrix4x4.Invert(Vector3sToMatrix4x4(SpatialTransform.Post), out _)) + confirmationMessage = "The spatial transform matrix is non-invertible. The transformed position data won't be properly output.\n\n"; + + confirmationMessage += "Would you like to continue?"; + + if (MessageBox.Show(confirmationMessage, "Confirmation", MessageBoxButtons.YesNo) == DialogResult.Yes) + DialogResult = DialogResult.OK; + } + } + + private readonly Func checkVector3ForNaN = v => new[] { v.X, v.Y, v.Z }.Any(float.IsNaN); + + private void EnableButtons(bool enable, byte index) { - var index = int.Parse((string)((TextBox)sender).Tag); - InputsValid[index] = true; - buttonCalculate.Enabled = InputsValid.All(inputValid => inputValid); + var buttons = new Button[] { buttonMeasure0, buttonMeasure1, buttonMeasure2, buttonMeasure3, buttonOK, buttonCancel }; + Array.ForEach(buttons, button => button.Enabled = enable || (Convert.ToByte(button.Tag) == index)); } - private void textBoxUserCoordinate_TextChanged(object sender, EventArgs e) + private void CalculatePrintMatrix() { - var index = int.Parse((string)((TextBox)sender).Tag); - string[] serInputSplit = ((TextBox)sender).Text.Split(','); - InputsValid[index] = serInputSplit.Length == 3 ? serInputSplit.All(floatCandidate => float.TryParse(floatCandidate, out _)) : false; - buttonCalculate.Enabled = InputsValid.All(inputValid => inputValid); + SpatialTransform.M = null; + if (!SpatialTransform.Post.Any(userCoordinate => checkVector3ForNaN(userCoordinate)) && + !SpatialTransform.Pre.Any(TS4231Coordinate => checkVector3ForNaN(TS4231Coordinate))) + { + if (Matrix4x4.Invert(Vector3sToMatrix4x4(SpatialTransform.Post), out _)) + { + var ts4231V1CoordinatesMatrix = Vector3sToMatrix4x4(SpatialTransform.Pre); + var userCoordinatesMatrix = Vector3sToMatrix4x4(SpatialTransform.Post); + Matrix4x4.Invert(ts4231V1CoordinatesMatrix, out var ts4231V1CoordinatesMatrixInverted); + SpatialTransform.M = Matrix4x4.Multiply(ts4231V1CoordinatesMatrixInverted, userCoordinatesMatrix); + toolStripStatusLabel.Image = Properties.Resources.StatusReadyImage; + toolStripStatusLabel.Text = "Spatial transform matrix is calculated."; + } + else + { + toolStripStatusLabel.Image = Properties.Resources.StatusWarningImage; + toolStripStatusLabel.Text = "The resulting spatial transform matrix must be non-invertible."; + } + } + else + { + toolStripStatusLabelUser.Image = OpenEphys.Onix1.Design.Properties.Resources.StatusBlockedImage; + toolStripStatusLabelUser.Text = "At least one user-defined coordinate is invalid."; + } + if (SpatialTransform.M.HasValue) + textBoxSpatialTransformMatrix.Text = SpatialTransform.M.Value.ToString(); + else + textBoxSpatialTransformMatrix.Text = ""; } - private void buttonCalculate_Click(object sender, EventArgs e) + private void ButtonCalculate_Click(object sender, EventArgs e) { var ts4231V1CoordinatesMatrix = new Matrix4x4( TS4231Coordinates[0].X, TS4231Coordinates[0].Y, TS4231Coordinates[0].Z, 1, - TS4231Coordinates[1].X, TS4231Coordinates[1].Y, TS4231Coordinates[1].Y, 1, + TS4231Coordinates[1].X, TS4231Coordinates[1].Y, TS4231Coordinates[1].Z, 1, TS4231Coordinates[2].X, TS4231Coordinates[2].Y, TS4231Coordinates[2].Z, 1, TS4231Coordinates[3].X, TS4231Coordinates[3].Y, TS4231Coordinates[3].Z, 1); @@ -118,23 +261,18 @@ private void buttonCalculate_Click(object sender, EventArgs e) userCoordinates[3][0], userCoordinates[3][1], userCoordinates[3][2], 1); Matrix4x4.Invert(ts4231V1CoordinatesMatrix, out var ts4231V1CoordinatesMatrixInverted); - SpatialTransform = Matrix4x4.Multiply(ts4231V1CoordinatesMatrixInverted, userCoordinatesMatrix); + NewSpatialTransform = Matrix4x4.Multiply(ts4231V1CoordinatesMatrixInverted, userCoordinatesMatrix); textBoxStatus.AppendText("The spatial transform matrix for the above coordinates is:" + Environment.NewLine); - textBoxStatus.AppendText(SpatialTransform.ToString() + Environment.NewLine + Environment.NewLine); + textBoxStatus.AppendText(NewSpatialTransform.ToString() + Environment.NewLine + Environment.NewLine); textBoxStatus.AppendText("Awaiting user input..." + Environment.NewLine); - checkBoxApplySpatialTransform.Enabled = true; + buttonOK.Enabled = true; } - private void buttonClose_Click(object sender, EventArgs e) + private void ButtonOKOrCancel_Click(object sender, EventArgs e) { Close(); } - - private void checkBoxApplySpatialTransform_CheckedChanged(object sender, EventArgs e) - { - ApplySpatialTransform = checkBoxApplySpatialTransform.Checked; - } } } diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.resx b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.resx index b6de2ecd..f9416718 100644 --- a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.resx +++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.resx @@ -118,13 +118,20 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Follow the instructions below to transform naive TS4231 position data from a naive reference frame to a user-defined reference frame. For more in-depth instructions, find the corresponding tutorial in Open Ephys' online documentation. -1) Make sure the workflow is running. -2) For each coordinate: + Follow the instructions below to transform naive TS4231 position data from the base-station reference frame to a user-defined reference frame. For more in-depth instructions, find the corresponding tutorial in Open Ephys' online documentation. +1) For each coordinate: • Place the TS4231V1 device and click the corresponding "Measure" button. - • Input how would like to define the coordinate in the user-defined reference frame. -3) Click the "Calculate Spatial Transform" button which enables after all fields are populated with valid data. -4) To automatically set the SpatialTransformMatrix property, check the bottom checkbox and close this GUI. + • Input how you would like to define the coordinate in the user-defined reference frame. +2) Click the "Calculate Spatial Transform" button which enables after all fields are populated with valid data. +3) Click "OK" to close this GUI and set the M property as the calculated spatial transform matrix. Click "Cancel" to close this GUI and not set the M property. + + + 36, 14 + + + All four user-defined coordinates must have the following format: "XX, YY, ZZ" or +"XX.XX, YY.YY, ZZ.ZZ" with any number of digits following the decimal before the +spatial transform matrix can be calculated. diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs b/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs index 2503a1a7..3a4bd790 100644 --- a/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs +++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs @@ -12,13 +12,14 @@ namespace OpenEphys.Onix1.Design { /// - /// Provides a user interface editor that displays a dialog for selecting - /// members of a workflow expression type. + /// Provides a user interface editor that displays a spatial-calibration dialog + /// for a . /// public class SpatialTransformMatrixEditor : DataSourceTypeEditor { + /// public SpatialTransformMatrixEditor() - : base(DataSource.Input, typeof(void)) + : base(DataSource.Output, typeof(void)) { } @@ -28,24 +29,23 @@ public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext contex return UITypeEditorEditStyle.Modal; } - protected virtual IObservable> GetData(IObservable> source) - { - return source.Merge().Select(coordinate => (Tuple)coordinate); - } - + /// public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value) { var editorService = (IWindowsFormsEditorService)provider.GetService(typeof(IWindowsFormsEditorService)); + var editorState = (IWorkflowEditorState)provider.GetService(typeof(IWorkflowEditorState)); if (context != null && editorService != null) { var source = GetDataSource(context, provider); - var dataFrames = GetData(source.Output); - using (var visualizerDialog = new SpatialTransformMatrixDialog(dataFrames)) + var dataFrames = source.Output.Merge().Select(x => x as TS4231V1PositionDataFrame); + using var visualizerDialog = new SpatialTransformMatrixDialog(dataFrames, new SpatialTransformProperties((SpatialTransformProperties)value)); + if (!editorState.WorkflowRunning) + { + throw new InvalidOperationException("Workflow must be running to open this GUI."); + } + else if (editorService.ShowDialog(visualizerDialog) == DialogResult.OK) { - if (editorService.ShowDialog(visualizerDialog) == DialogResult.OK && visualizerDialog.ApplySpatialTransform) - { - return visualizerDialog.SpatialTransform; - } + return visualizerDialog.NewSpatialTransform; } } return base.EditValue(context, provider, value); diff --git a/OpenEphys.Onix1/SpatialTransform.cs b/OpenEphys.Onix1/SpatialTransform.cs deleted file mode 100644 index 49dab035..00000000 --- a/OpenEphys.Onix1/SpatialTransform.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Bonsai; -using System; -using System.ComponentModel; -using System.Linq; -using System.Reactive.Linq; -using System.Numerics; - -namespace OpenEphys.Onix1 -{ - /// - /// Represents an operator that groups the elements of an observable - /// sequence according to the specified key. - /// - [DefaultProperty(nameof(SpatialTransformMatrix))] - [Description("Transforms 3D coordinates from one reference frame to another.")] - public class SpatialTransform : Transform, Vector3> - { - /// - /// Gets or sets a value specifying the inner properties used as key for - /// each element in the sequence. - /// - [Description("Spatial Transform Matrix")] - [Editor("OpenEphys.Onix1.Design.SpatialTransformMatrixEditor, OpenEphys.Onix1.Design", DesignTypes.UITypeEditor)] - [TypeConverter(typeof(NumericRecordConverter))] - public Matrix4x4 SpatialTransformMatrix { get; set; } = new Matrix4x4( - 1, 0, 0, 0, - 0, 1, 0, 0, - 0, 0, 1, 0, - 0, 0, 0, 1); - - public override IObservable Process(IObservable> source) - { - return source.Select(input => Vector3.Transform(input.Item2, this.SpatialTransformMatrix)); - } - } -} diff --git a/OpenEphys.Onix1/SpatialTransform3D.cs b/OpenEphys.Onix1/SpatialTransform3D.cs new file mode 100644 index 00000000..a642e2df --- /dev/null +++ b/OpenEphys.Onix1/SpatialTransform3D.cs @@ -0,0 +1,79 @@ +using System; +using System.Linq; +using System.Numerics; +using System.Xml.Serialization; + +namespace OpenEphys.Onix1 +{ + /// + /// Data necessary to construct a spatial transform matrix as well as the + /// spatial transform matrix itself. + /// + public class SpatialTransform3D + { + + Matrix4x4 a, b; + + /// + /// The A matrix in A * = . It is + /// constructed from a set of four pre-transform Cartesian coordinates. + /// + public Matrix4x4 A { get => a; set { a = value; M = UpdateM(A, B); } } + + /// + /// The B matrix in * = B. It is + /// constructed from a set of four post-transform Cartesian coordinates. + /// + public Matrix4x4 B { get => b; set { b = value; M = UpdateM(A, B); } } + + /// + /// The M matrix in * = M. It is the + /// spatial transform matrix. It calculated as M = A.inv * B. + /// + [XmlIgnore] + public Matrix4x4 M { get; private set; } + + /// + /// Initializes a new instance of the + /// class with default values. + /// + public SpatialTransform3D() + { + A = B = new(float.NaN, float.NaN, float.NaN, 1, + float.NaN, float.NaN, float.NaN, 1, + float.NaN, float.NaN, float.NaN, 1, + float.NaN, float.NaN, float.NaN, 1); + M = new(float.NaN, float.NaN, float.NaN, float.NaN, + float.NaN, float.NaN, float.NaN, float.NaN, + float.NaN, float.NaN, float.NaN, float.NaN, + float.NaN, float.NaN, float.NaN, float.NaN); + } + + /// + /// Initializes a new instance of the + /// class as a copy of an existing instance. + /// + /// The instance to copy. + public SpatialTransform3D(SpatialTransform3D other) + { + A = other.A; + B = other.B; + } + + static Matrix4x4 UpdateM(Matrix4x4 a, Matrix4x4 b) + { + Matrix4x4.Invert(a, out var aInverted); + var m = Matrix4x4.Multiply(aInverted, b); + if (Matrix4x4.Invert(m, out _) && !new float[] { m.M11, m.M12, m.M13, m.M14, + m.M21, m.M22, m.M23, m.M24, + m.M31, m.M32, m.M33, m.M34, + m.M41, m.M42, m.M43, m.M44 }.Any(float.IsNaN)) + return m; + else + return new(float.NaN, float.NaN, float.NaN, float.NaN, + float.NaN, float.NaN, float.NaN, float.NaN, + float.NaN, float.NaN, float.NaN, float.NaN, + float.NaN, float.NaN, float.NaN, float.NaN); + } + } +} \ No newline at end of file diff --git a/OpenEphys.Onix1/TS4231V1SpatialTransform.cs b/OpenEphys.Onix1/TS4231V1SpatialTransform.cs new file mode 100644 index 00000000..74751300 --- /dev/null +++ b/OpenEphys.Onix1/TS4231V1SpatialTransform.cs @@ -0,0 +1,40 @@ +using System; +using System.ComponentModel; +using System.Numerics; +using System.Reactive.Linq; +using Bonsai; + +namespace OpenEphys.Onix1 +{ + /// + /// Transforms a sequence of 3D positions from to an external coordinate system. + /// + [DefaultProperty(nameof(SpatialTransform))] + public class TS4231V1SpatialTransform : Transform + { + /// + /// Gets or sets the pre- and post- transform coordinates to calculate + /// the spatial transform matrix as well as the spatial transform matrix + /// itself. + /// + [Editor("OpenEphys.Onix1.Design.SpatialTransformMatrixEditor, OpenEphys.Onix1.Design", DesignTypes.UITypeEditor)] + [Description("Data for transforming position measurements to another reference frame.")] + public SpatialTransform3D SpatialTransform { get; set; } = new(); + + /// + /// Transforms a sequence of + /// objects, each of which contains transformed 3D position of single + /// photodiode. + /// + /// + /// A sequence of objects with + /// transformed position data. + /// + public override IObservable Process(IObservable source) + { + return source.Select(input => + new TS4231V1PositionDataFrame(input.Clock, input.HubClock, input.SensorIndex, + Vector3.Transform(input.Position, SpatialTransform.M))); + } + } +} From cc4eee65730c79104003103304567967547a17a3 Mon Sep 17 00:00:00 2001 From: cjsha Date: Mon, 18 Aug 2025 16:41:27 -0400 Subject: [PATCH 15/17] Dialog changes - Add X, Y, Z labels - Add a textbox for each component of each coordinate instead of one textbox - Add a textbox & label for displaying Spatial Transform Matrix - Change status messages TextBox to RichTextBox which allows changing font color and using newline characters instead of environment.newline. - Automatically calculate transform matrix when inputs are valid (avoids decoupling sets of pre-transform & post-transform coordinates and the spatial transform) - Simplify bottom toolstrip behavior - Move event handlers to top and helper methods underneath - Change instructions in top label according to the above changes - Change some text in the confirmation dialog - Remove private access modifiers where they're not necessary --- .../SpatialTransformMatrixDialog.Designer.cs | 618 +++++++++++------- .../SpatialTransformMatrixDialog.cs | 285 ++++---- .../SpatialTransformMatrixDialog.resx | 24 +- .../SpatialTransformMatrixEditor.cs | 11 +- 4 files changed, 534 insertions(+), 404 deletions(-) diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.Designer.cs b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.Designer.cs index 17e2994e..39c04818 100644 --- a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.Designer.cs +++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.Designer.cs @@ -33,13 +33,23 @@ private void InitializeComponent() System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(SpatialTransformMatrixDialog)); this.tableLayoutPanelMain = new System.Windows.Forms.TableLayoutPanel(); this.groupBoxStatus = new System.Windows.Forms.GroupBox(); - this.textBoxStatus = new System.Windows.Forms.TextBox(); + this.richTextBoxStatus = new System.Windows.Forms.RichTextBox(); this.labelInstructions = new System.Windows.Forms.Label(); this.tableLayoutPanelCoordinates = new System.Windows.Forms.TableLayoutPanel(); - this.textBoxUserCoordinate3 = new System.Windows.Forms.TextBox(); - this.textBoxUserCoordinate2 = new System.Windows.Forms.TextBox(); - this.textBoxUserCoordinate1 = new System.Windows.Forms.TextBox(); - this.textBoxUserCoordinate0 = new System.Windows.Forms.TextBox(); + this.labelZ = new System.Windows.Forms.Label(); + this.labelY = new System.Windows.Forms.Label(); + this.textBoxUserCoordinate3Z = new System.Windows.Forms.TextBox(); + this.textBoxUserCoordinate3Y = new System.Windows.Forms.TextBox(); + this.textBoxUserCoordinate3X = new System.Windows.Forms.TextBox(); + this.textBoxUserCoordinate2Z = new System.Windows.Forms.TextBox(); + this.textBoxUserCoordinate2Y = new System.Windows.Forms.TextBox(); + this.textBoxUserCoordinate2X = new System.Windows.Forms.TextBox(); + this.textBoxUserCoordinate1Z = new System.Windows.Forms.TextBox(); + this.textBoxUserCoordinate1Y = new System.Windows.Forms.TextBox(); + this.textBoxUserCoordinate0Z = new System.Windows.Forms.TextBox(); + this.textBoxUserCoordinate0Y = new System.Windows.Forms.TextBox(); + this.textBoxUserCoordinate1X = new System.Windows.Forms.TextBox(); + this.labelXyz = new System.Windows.Forms.Label(); this.textBoxTS4231Coordinate3 = new System.Windows.Forms.TextBox(); this.textBoxTS4231Coordinate2 = new System.Windows.Forms.TextBox(); this.textBoxTS4231Coordinate1 = new System.Windows.Forms.TextBox(); @@ -50,21 +60,25 @@ private void InitializeComponent() this.labelCoordinate2 = new System.Windows.Forms.Label(); this.labelCoordinate1 = new System.Windows.Forms.Label(); this.buttonMeasure0 = new System.Windows.Forms.Button(); - this.labelHeaderTS4231 = new System.Windows.Forms.Label(); + this.labelTS4231 = new System.Windows.Forms.Label(); this.labelCoordinate0 = new System.Windows.Forms.Label(); this.textBoxTS4231Coordinate0 = new System.Windows.Forms.TextBox(); - this.labelHeaderUser = new System.Windows.Forms.Label(); - this.buttonCalculate = new System.Windows.Forms.Button(); + this.textBoxUserCoordinate0X = new System.Windows.Forms.TextBox(); + this.labelUser = new System.Windows.Forms.Label(); + this.labelX = new System.Windows.Forms.Label(); this.flowLayoutPanelBottom = new System.Windows.Forms.FlowLayoutPanel(); this.buttonCancel = new System.Windows.Forms.Button(); this.buttonOK = new System.Windows.Forms.Button(); + this.tableLayoutPanelSpatialMatrix = new System.Windows.Forms.TableLayoutPanel(); + this.labelSpatialMatrix = new System.Windows.Forms.Label(); + this.textBoxSpatialTransformMatrix = new System.Windows.Forms.TextBox(); this.statusStrip = new System.Windows.Forms.StatusStrip(); - this.toolStripStatusLabelTS4231 = new System.Windows.Forms.ToolStripStatusLabel(); - this.toolStripStatusLabelUser = new System.Windows.Forms.ToolStripStatusLabel(); + this.toolStripStatusLabel = new System.Windows.Forms.ToolStripStatusLabel(); this.tableLayoutPanelMain.SuspendLayout(); this.groupBoxStatus.SuspendLayout(); this.tableLayoutPanelCoordinates.SuspendLayout(); this.flowLayoutPanelBottom.SuspendLayout(); + this.tableLayoutPanelSpatialMatrix.SuspendLayout(); this.statusStrip.SuspendLayout(); this.SuspendLayout(); // @@ -72,183 +86,292 @@ private void InitializeComponent() // this.tableLayoutPanelMain.ColumnCount = 1; this.tableLayoutPanelMain.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); - this.tableLayoutPanelMain.Controls.Add(this.groupBoxStatus, 0, 2); + this.tableLayoutPanelMain.Controls.Add(this.groupBoxStatus, 0, 3); this.tableLayoutPanelMain.Controls.Add(this.labelInstructions, 0, 0); this.tableLayoutPanelMain.Controls.Add(this.tableLayoutPanelCoordinates, 0, 1); - this.tableLayoutPanelMain.Controls.Add(this.buttonCalculate, 0, 3); this.tableLayoutPanelMain.Controls.Add(this.flowLayoutPanelBottom, 0, 4); + this.tableLayoutPanelMain.Controls.Add(this.tableLayoutPanelSpatialMatrix, 0, 2); this.tableLayoutPanelMain.Dock = System.Windows.Forms.DockStyle.Fill; this.tableLayoutPanelMain.Location = new System.Drawing.Point(0, 0); this.tableLayoutPanelMain.Name = "tableLayoutPanelMain"; this.tableLayoutPanelMain.RowCount = 5; this.tableLayoutPanelMain.RowStyles.Add(new System.Windows.Forms.RowStyle()); this.tableLayoutPanelMain.RowStyles.Add(new System.Windows.Forms.RowStyle()); - this.tableLayoutPanelMain.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F)); this.tableLayoutPanelMain.RowStyles.Add(new System.Windows.Forms.RowStyle()); + this.tableLayoutPanelMain.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F)); this.tableLayoutPanelMain.RowStyles.Add(new System.Windows.Forms.RowStyle()); - this.tableLayoutPanelMain.Size = new System.Drawing.Size(604, 639); - this.tableLayoutPanelMain.TabIndex = 7; + this.tableLayoutPanelMain.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 20F)); + this.tableLayoutPanelMain.Size = new System.Drawing.Size(624, 540); + this.tableLayoutPanelMain.TabIndex = 0; // // groupBoxStatus // - this.groupBoxStatus.Controls.Add(this.textBoxStatus); + this.groupBoxStatus.Controls.Add(this.richTextBoxStatus); this.groupBoxStatus.Dock = System.Windows.Forms.DockStyle.Fill; - this.groupBoxStatus.Location = new System.Drawing.Point(3, 263); + this.groupBoxStatus.Location = new System.Drawing.Point(3, 372); this.groupBoxStatus.Name = "groupBoxStatus"; - this.groupBoxStatus.Size = new System.Drawing.Size(598, 308); - this.groupBoxStatus.TabIndex = 6; + this.groupBoxStatus.Size = new System.Drawing.Size(618, 129); + this.groupBoxStatus.TabIndex = 1000; this.groupBoxStatus.TabStop = false; this.groupBoxStatus.Text = "Status Messages"; // - // textBoxStatus + // richTextBoxStatus // - this.textBoxStatus.AcceptsReturn = true; - this.textBoxStatus.AcceptsTab = true; - this.textBoxStatus.Dock = System.Windows.Forms.DockStyle.Fill; - this.textBoxStatus.Location = new System.Drawing.Point(3, 16); - this.textBoxStatus.Multiline = true; - this.textBoxStatus.Name = "textBoxStatus"; - this.textBoxStatus.ReadOnly = true; - this.textBoxStatus.ScrollBars = System.Windows.Forms.ScrollBars.Vertical; - this.textBoxStatus.Size = new System.Drawing.Size(592, 289); - this.textBoxStatus.TabIndex = 3; - this.textBoxStatus.Text = "Awaiting user input...\r\n"; + this.richTextBoxStatus.Dock = System.Windows.Forms.DockStyle.Fill; + this.richTextBoxStatus.Location = new System.Drawing.Point(3, 16); + this.richTextBoxStatus.Name = "richTextBoxStatus"; + this.richTextBoxStatus.ReadOnly = true; + this.richTextBoxStatus.ScrollBars = System.Windows.Forms.RichTextBoxScrollBars.ForcedVertical; + this.richTextBoxStatus.Size = new System.Drawing.Size(612, 110); + this.richTextBoxStatus.TabIndex = 1000; + this.richTextBoxStatus.TabStop = false; + this.richTextBoxStatus.Text = ""; // // labelInstructions // this.labelInstructions.AutoSize = true; this.labelInstructions.Location = new System.Drawing.Point(3, 0); this.labelInstructions.Name = "labelInstructions"; - this.labelInstructions.Size = new System.Drawing.Size(596, 104); - this.labelInstructions.TabIndex = 4; + this.labelInstructions.Size = new System.Drawing.Size(596, 117); + this.labelInstructions.TabIndex = 1000; this.labelInstructions.Text = resources.GetString("labelInstructions.Text"); // // tableLayoutPanelCoordinates // - this.tableLayoutPanelCoordinates.AutoSize = true; - this.tableLayoutPanelCoordinates.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink; - this.tableLayoutPanelCoordinates.ColumnCount = 4; + this.tableLayoutPanelCoordinates.ColumnCount = 6; this.tableLayoutPanelCoordinates.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); this.tableLayoutPanelCoordinates.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); - this.tableLayoutPanelCoordinates.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F)); - this.tableLayoutPanelCoordinates.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 50F)); - this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxUserCoordinate3, 3, 4); - this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxUserCoordinate2, 3, 3); - this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxUserCoordinate1, 3, 2); - this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxUserCoordinate0, 3, 1); - this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxTS4231Coordinate3, 2, 4); - this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxTS4231Coordinate2, 2, 3); - this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxTS4231Coordinate1, 2, 2); - this.tableLayoutPanelCoordinates.Controls.Add(this.buttonMeasure3, 1, 4); - this.tableLayoutPanelCoordinates.Controls.Add(this.buttonMeasure2, 1, 3); - this.tableLayoutPanelCoordinates.Controls.Add(this.buttonMeasure1, 1, 2); - this.tableLayoutPanelCoordinates.Controls.Add(this.labelCoordinate3, 0, 4); - this.tableLayoutPanelCoordinates.Controls.Add(this.labelCoordinate2, 0, 3); - this.tableLayoutPanelCoordinates.Controls.Add(this.labelCoordinate1, 0, 2); - this.tableLayoutPanelCoordinates.Controls.Add(this.buttonMeasure0, 1, 1); - this.tableLayoutPanelCoordinates.Controls.Add(this.labelHeaderTS4231, 1, 0); - this.tableLayoutPanelCoordinates.Controls.Add(this.labelCoordinate0, 0, 1); - this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxTS4231Coordinate0, 2, 1); - this.tableLayoutPanelCoordinates.Controls.Add(this.labelHeaderUser, 3, 0); + this.tableLayoutPanelCoordinates.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 180F)); + this.tableLayoutPanelCoordinates.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 33.33333F)); + this.tableLayoutPanelCoordinates.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 33.33334F)); + this.tableLayoutPanelCoordinates.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 33.33334F)); + this.tableLayoutPanelCoordinates.Controls.Add(this.labelZ, 6, 1); + this.tableLayoutPanelCoordinates.Controls.Add(this.labelY, 4, 1); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxUserCoordinate3Z, 5, 5); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxUserCoordinate3Y, 4, 5); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxUserCoordinate3X, 3, 5); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxUserCoordinate2Z, 5, 4); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxUserCoordinate2Y, 4, 4); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxUserCoordinate2X, 3, 4); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxUserCoordinate1Z, 5, 3); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxUserCoordinate1Y, 4, 3); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxUserCoordinate0Z, 5, 2); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxUserCoordinate0Y, 4, 2); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxUserCoordinate1X, 3, 3); + this.tableLayoutPanelCoordinates.Controls.Add(this.labelXyz, 2, 1); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxTS4231Coordinate3, 2, 5); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxTS4231Coordinate2, 2, 4); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxTS4231Coordinate1, 2, 3); + this.tableLayoutPanelCoordinates.Controls.Add(this.buttonMeasure3, 1, 5); + this.tableLayoutPanelCoordinates.Controls.Add(this.buttonMeasure2, 1, 4); + this.tableLayoutPanelCoordinates.Controls.Add(this.buttonMeasure1, 1, 3); + this.tableLayoutPanelCoordinates.Controls.Add(this.labelCoordinate3, 0, 5); + this.tableLayoutPanelCoordinates.Controls.Add(this.labelCoordinate2, 0, 4); + this.tableLayoutPanelCoordinates.Controls.Add(this.labelCoordinate1, 0, 3); + this.tableLayoutPanelCoordinates.Controls.Add(this.buttonMeasure0, 1, 2); + this.tableLayoutPanelCoordinates.Controls.Add(this.labelTS4231, 1, 0); + this.tableLayoutPanelCoordinates.Controls.Add(this.labelCoordinate0, 0, 2); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxTS4231Coordinate0, 2, 2); + this.tableLayoutPanelCoordinates.Controls.Add(this.textBoxUserCoordinate0X, 3, 2); + this.tableLayoutPanelCoordinates.Controls.Add(this.labelUser, 3, 0); + this.tableLayoutPanelCoordinates.Controls.Add(this.labelX, 3, 1); this.tableLayoutPanelCoordinates.Dock = System.Windows.Forms.DockStyle.Fill; - this.tableLayoutPanelCoordinates.Location = new System.Drawing.Point(3, 107); + this.tableLayoutPanelCoordinates.Location = new System.Drawing.Point(3, 120); this.tableLayoutPanelCoordinates.Name = "tableLayoutPanelCoordinates"; - this.tableLayoutPanelCoordinates.RowCount = 5; + this.tableLayoutPanelCoordinates.RowCount = 6; + this.tableLayoutPanelCoordinates.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 30F)); this.tableLayoutPanelCoordinates.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 30F)); this.tableLayoutPanelCoordinates.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 30F)); this.tableLayoutPanelCoordinates.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 30F)); this.tableLayoutPanelCoordinates.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 30F)); this.tableLayoutPanelCoordinates.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 30F)); - this.tableLayoutPanelCoordinates.Size = new System.Drawing.Size(598, 150); - this.tableLayoutPanelCoordinates.TabIndex = 5; - this.tableLayoutPanelCoordinates.Tag = "6"; - // - // textBoxUserCoordinate3 - // - this.textBoxUserCoordinate3.Dock = System.Windows.Forms.DockStyle.Fill; - this.textBoxUserCoordinate3.Location = new System.Drawing.Point(385, 123); - this.textBoxUserCoordinate3.MinimumSize = new System.Drawing.Size(150, 4); - this.textBoxUserCoordinate3.Name = "textBoxUserCoordinate3"; - this.textBoxUserCoordinate3.Size = new System.Drawing.Size(210, 20); - this.textBoxUserCoordinate3.TabIndex = 39; - this.textBoxUserCoordinate3.Tag = "7"; - this.textBoxUserCoordinate3.TextChanged += new System.EventHandler(this.TextBoxUserCoordinate_TextChanged); - // - // textBoxUserCoordinate2 - // - this.textBoxUserCoordinate2.Dock = System.Windows.Forms.DockStyle.Fill; - this.textBoxUserCoordinate2.Location = new System.Drawing.Point(385, 93); - this.textBoxUserCoordinate2.MinimumSize = new System.Drawing.Size(150, 4); - this.textBoxUserCoordinate2.Name = "textBoxUserCoordinate2"; - this.textBoxUserCoordinate2.Size = new System.Drawing.Size(210, 20); - this.textBoxUserCoordinate2.TabIndex = 38; - this.textBoxUserCoordinate2.Tag = "6"; - this.textBoxUserCoordinate2.TextChanged += new System.EventHandler(this.TextBoxUserCoordinate_TextChanged); - // - // textBoxUserCoordinate1 - // - this.textBoxUserCoordinate1.Dock = System.Windows.Forms.DockStyle.Fill; - this.textBoxUserCoordinate1.Location = new System.Drawing.Point(385, 63); - this.textBoxUserCoordinate1.MinimumSize = new System.Drawing.Size(150, 4); - this.textBoxUserCoordinate1.Name = "textBoxUserCoordinate1"; - this.textBoxUserCoordinate1.Size = new System.Drawing.Size(210, 20); - this.textBoxUserCoordinate1.TabIndex = 37; - this.textBoxUserCoordinate1.Tag = "5"; - this.textBoxUserCoordinate1.TextChanged += new System.EventHandler(this.TextBoxUserCoordinate_TextChanged); - // - // textBoxUserCoordinate0 - // - this.textBoxUserCoordinate0.Dock = System.Windows.Forms.DockStyle.Fill; - this.textBoxUserCoordinate0.Location = new System.Drawing.Point(385, 33); - this.textBoxUserCoordinate0.MinimumSize = new System.Drawing.Size(150, 4); - this.textBoxUserCoordinate0.Name = "textBoxUserCoordinate0"; - this.textBoxUserCoordinate0.Size = new System.Drawing.Size(210, 20); - this.textBoxUserCoordinate0.TabIndex = 36; - this.textBoxUserCoordinate0.Tag = "4"; - this.textBoxUserCoordinate0.TextChanged += new System.EventHandler(this.TextBoxUserCoordinate_TextChanged); + this.tableLayoutPanelCoordinates.Size = new System.Drawing.Size(618, 180); + this.tableLayoutPanelCoordinates.TabIndex = 0; + this.tableLayoutPanelCoordinates.Tag = "0"; + // + // labelZ + // + this.labelZ.Dock = System.Windows.Forms.DockStyle.Fill; + this.labelZ.Location = new System.Drawing.Point(526, 30); + this.labelZ.Margin = new System.Windows.Forms.Padding(0); + this.labelZ.Name = "labelZ"; + this.labelZ.Size = new System.Drawing.Size(92, 30); + this.labelZ.TabIndex = 1000; + this.labelZ.Text = "Z"; + this.labelZ.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // labelY + // + this.labelY.Dock = System.Windows.Forms.DockStyle.Fill; + this.labelY.Location = new System.Drawing.Point(436, 30); + this.labelY.Margin = new System.Windows.Forms.Padding(0); + this.labelY.Name = "labelY"; + this.labelY.Size = new System.Drawing.Size(90, 30); + this.labelY.TabIndex = 1000; + this.labelY.Text = "Y"; + this.labelY.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // textBoxUserCoordinate3Z + // + this.textBoxUserCoordinate3Z.Dock = System.Windows.Forms.DockStyle.Bottom; + this.textBoxUserCoordinate3Z.Location = new System.Drawing.Point(529, 157); + this.textBoxUserCoordinate3Z.Name = "textBoxUserCoordinate3Z"; + this.textBoxUserCoordinate3Z.Size = new System.Drawing.Size(86, 20); + this.textBoxUserCoordinate3Z.TabIndex = 15; + this.textBoxUserCoordinate3Z.Tag = "11"; + this.textBoxUserCoordinate3Z.TextChanged += new System.EventHandler(this.TextBoxUserCoordinate_TextChanged); + // + // textBoxUserCoordinate3Y + // + this.textBoxUserCoordinate3Y.Dock = System.Windows.Forms.DockStyle.Bottom; + this.textBoxUserCoordinate3Y.Location = new System.Drawing.Point(439, 157); + this.textBoxUserCoordinate3Y.Name = "textBoxUserCoordinate3Y"; + this.textBoxUserCoordinate3Y.Size = new System.Drawing.Size(84, 20); + this.textBoxUserCoordinate3Y.TabIndex = 14; + this.textBoxUserCoordinate3Y.Tag = "10"; + this.textBoxUserCoordinate3Y.TextChanged += new System.EventHandler(this.TextBoxUserCoordinate_TextChanged); + // + // textBoxUserCoordinate3X + // + this.textBoxUserCoordinate3X.Dock = System.Windows.Forms.DockStyle.Bottom; + this.textBoxUserCoordinate3X.Location = new System.Drawing.Point(349, 157); + this.textBoxUserCoordinate3X.Name = "textBoxUserCoordinate3X"; + this.textBoxUserCoordinate3X.Size = new System.Drawing.Size(84, 20); + this.textBoxUserCoordinate3X.TabIndex = 13; + this.textBoxUserCoordinate3X.Tag = "9"; + this.textBoxUserCoordinate3X.TextChanged += new System.EventHandler(this.TextBoxUserCoordinate_TextChanged); + // + // textBoxUserCoordinate2Z + // + this.textBoxUserCoordinate2Z.Dock = System.Windows.Forms.DockStyle.Bottom; + this.textBoxUserCoordinate2Z.Location = new System.Drawing.Point(529, 127); + this.textBoxUserCoordinate2Z.Name = "textBoxUserCoordinate2Z"; + this.textBoxUserCoordinate2Z.Size = new System.Drawing.Size(86, 20); + this.textBoxUserCoordinate2Z.TabIndex = 11; + this.textBoxUserCoordinate2Z.Tag = "8"; + this.textBoxUserCoordinate2Z.TextChanged += new System.EventHandler(this.TextBoxUserCoordinate_TextChanged); + // + // textBoxUserCoordinate2Y + // + this.textBoxUserCoordinate2Y.Dock = System.Windows.Forms.DockStyle.Bottom; + this.textBoxUserCoordinate2Y.Location = new System.Drawing.Point(439, 127); + this.textBoxUserCoordinate2Y.Name = "textBoxUserCoordinate2Y"; + this.textBoxUserCoordinate2Y.Size = new System.Drawing.Size(84, 20); + this.textBoxUserCoordinate2Y.TabIndex = 10; + this.textBoxUserCoordinate2Y.Tag = "7"; + this.textBoxUserCoordinate2Y.TextChanged += new System.EventHandler(this.TextBoxUserCoordinate_TextChanged); + // + // textBoxUserCoordinate2X + // + this.textBoxUserCoordinate2X.Dock = System.Windows.Forms.DockStyle.Bottom; + this.textBoxUserCoordinate2X.Location = new System.Drawing.Point(349, 127); + this.textBoxUserCoordinate2X.Name = "textBoxUserCoordinate2X"; + this.textBoxUserCoordinate2X.Size = new System.Drawing.Size(84, 20); + this.textBoxUserCoordinate2X.TabIndex = 9; + this.textBoxUserCoordinate2X.Tag = "6"; + this.textBoxUserCoordinate2X.TextChanged += new System.EventHandler(this.TextBoxUserCoordinate_TextChanged); + // + // textBoxUserCoordinate1Z + // + this.textBoxUserCoordinate1Z.Dock = System.Windows.Forms.DockStyle.Bottom; + this.textBoxUserCoordinate1Z.Location = new System.Drawing.Point(529, 97); + this.textBoxUserCoordinate1Z.Name = "textBoxUserCoordinate1Z"; + this.textBoxUserCoordinate1Z.Size = new System.Drawing.Size(86, 20); + this.textBoxUserCoordinate1Z.TabIndex = 7; + this.textBoxUserCoordinate1Z.Tag = "5"; + this.textBoxUserCoordinate1Z.TextChanged += new System.EventHandler(this.TextBoxUserCoordinate_TextChanged); + // + // textBoxUserCoordinate1Y + // + this.textBoxUserCoordinate1Y.Dock = System.Windows.Forms.DockStyle.Bottom; + this.textBoxUserCoordinate1Y.Location = new System.Drawing.Point(439, 97); + this.textBoxUserCoordinate1Y.Name = "textBoxUserCoordinate1Y"; + this.textBoxUserCoordinate1Y.Size = new System.Drawing.Size(84, 20); + this.textBoxUserCoordinate1Y.TabIndex = 6; + this.textBoxUserCoordinate1Y.Tag = "4"; + this.textBoxUserCoordinate1Y.TextChanged += new System.EventHandler(this.TextBoxUserCoordinate_TextChanged); + // + // textBoxUserCoordinate0Z + // + this.textBoxUserCoordinate0Z.Dock = System.Windows.Forms.DockStyle.Bottom; + this.textBoxUserCoordinate0Z.Location = new System.Drawing.Point(529, 67); + this.textBoxUserCoordinate0Z.Name = "textBoxUserCoordinate0Z"; + this.textBoxUserCoordinate0Z.Size = new System.Drawing.Size(86, 20); + this.textBoxUserCoordinate0Z.TabIndex = 3; + this.textBoxUserCoordinate0Z.Tag = "2"; + this.textBoxUserCoordinate0Z.TextChanged += new System.EventHandler(this.TextBoxUserCoordinate_TextChanged); + // + // textBoxUserCoordinate0Y + // + this.textBoxUserCoordinate0Y.Dock = System.Windows.Forms.DockStyle.Bottom; + this.textBoxUserCoordinate0Y.Location = new System.Drawing.Point(439, 67); + this.textBoxUserCoordinate0Y.Name = "textBoxUserCoordinate0Y"; + this.textBoxUserCoordinate0Y.Size = new System.Drawing.Size(84, 20); + this.textBoxUserCoordinate0Y.TabIndex = 2; + this.textBoxUserCoordinate0Y.Tag = "1"; + this.textBoxUserCoordinate0Y.TextChanged += new System.EventHandler(this.TextBoxUserCoordinate_TextChanged); + // + // textBoxUserCoordinate1X + // + this.textBoxUserCoordinate1X.Dock = System.Windows.Forms.DockStyle.Bottom; + this.textBoxUserCoordinate1X.Location = new System.Drawing.Point(349, 97); + this.textBoxUserCoordinate1X.Name = "textBoxUserCoordinate1X"; + this.textBoxUserCoordinate1X.Size = new System.Drawing.Size(84, 20); + this.textBoxUserCoordinate1X.TabIndex = 5; + this.textBoxUserCoordinate1X.Tag = "3"; + this.textBoxUserCoordinate1X.TextChanged += new System.EventHandler(this.TextBoxUserCoordinate_TextChanged); + // + // labelXyz + // + this.labelXyz.Dock = System.Windows.Forms.DockStyle.Fill; + this.labelXyz.Location = new System.Drawing.Point(166, 30); + this.labelXyz.Margin = new System.Windows.Forms.Padding(0); + this.labelXyz.Name = "labelXyz"; + this.labelXyz.Size = new System.Drawing.Size(180, 30); + this.labelXyz.TabIndex = 1000; + this.labelXyz.Text = "X, Y, Z"; + this.labelXyz.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; // // textBoxTS4231Coordinate3 // - this.textBoxTS4231Coordinate3.Dock = System.Windows.Forms.DockStyle.Fill; - this.textBoxTS4231Coordinate3.Location = new System.Drawing.Point(169, 123); - this.textBoxTS4231Coordinate3.MinimumSize = new System.Drawing.Size(150, 4); + this.textBoxTS4231Coordinate3.Dock = System.Windows.Forms.DockStyle.Bottom; + this.textBoxTS4231Coordinate3.Location = new System.Drawing.Point(169, 157); this.textBoxTS4231Coordinate3.Name = "textBoxTS4231Coordinate3"; this.textBoxTS4231Coordinate3.ReadOnly = true; - this.textBoxTS4231Coordinate3.Size = new System.Drawing.Size(210, 20); - this.textBoxTS4231Coordinate3.TabIndex = 33; + this.textBoxTS4231Coordinate3.Size = new System.Drawing.Size(174, 20); + this.textBoxTS4231Coordinate3.TabIndex = 1000; this.textBoxTS4231Coordinate3.TabStop = false; this.textBoxTS4231Coordinate3.Tag = "3"; // // textBoxTS4231Coordinate2 // - this.textBoxTS4231Coordinate2.Dock = System.Windows.Forms.DockStyle.Fill; - this.textBoxTS4231Coordinate2.Location = new System.Drawing.Point(169, 93); - this.textBoxTS4231Coordinate2.MinimumSize = new System.Drawing.Size(150, 4); + this.textBoxTS4231Coordinate2.Dock = System.Windows.Forms.DockStyle.Bottom; + this.textBoxTS4231Coordinate2.Location = new System.Drawing.Point(169, 127); this.textBoxTS4231Coordinate2.Name = "textBoxTS4231Coordinate2"; this.textBoxTS4231Coordinate2.ReadOnly = true; - this.textBoxTS4231Coordinate2.Size = new System.Drawing.Size(210, 20); - this.textBoxTS4231Coordinate2.TabIndex = 32; + this.textBoxTS4231Coordinate2.Size = new System.Drawing.Size(174, 20); + this.textBoxTS4231Coordinate2.TabIndex = 1000; this.textBoxTS4231Coordinate2.TabStop = false; this.textBoxTS4231Coordinate2.Tag = "2"; // // textBoxTS4231Coordinate1 // - this.textBoxTS4231Coordinate1.Dock = System.Windows.Forms.DockStyle.Fill; - this.textBoxTS4231Coordinate1.Location = new System.Drawing.Point(169, 63); - this.textBoxTS4231Coordinate1.MinimumSize = new System.Drawing.Size(150, 4); + this.textBoxTS4231Coordinate1.Dock = System.Windows.Forms.DockStyle.Bottom; + this.textBoxTS4231Coordinate1.Location = new System.Drawing.Point(169, 97); this.textBoxTS4231Coordinate1.Name = "textBoxTS4231Coordinate1"; this.textBoxTS4231Coordinate1.ReadOnly = true; - this.textBoxTS4231Coordinate1.Size = new System.Drawing.Size(210, 20); - this.textBoxTS4231Coordinate1.TabIndex = 31; + this.textBoxTS4231Coordinate1.Size = new System.Drawing.Size(174, 20); + this.textBoxTS4231Coordinate1.TabIndex = 1000; this.textBoxTS4231Coordinate1.TabStop = false; this.textBoxTS4231Coordinate1.Tag = "1"; // // buttonMeasure3 // - this.buttonMeasure3.Location = new System.Drawing.Point(83, 123); + this.buttonMeasure3.Anchor = System.Windows.Forms.AnchorStyles.Left; + this.buttonMeasure3.Location = new System.Drawing.Point(83, 153); this.buttonMeasure3.Name = "buttonMeasure3"; this.buttonMeasure3.Size = new System.Drawing.Size(80, 24); - this.buttonMeasure3.TabIndex = 29; + this.buttonMeasure3.TabIndex = 12; this.buttonMeasure3.Tag = "3"; this.buttonMeasure3.Text = "Measure"; this.buttonMeasure3.UseVisualStyleBackColor = true; @@ -256,10 +379,11 @@ private void InitializeComponent() // // buttonMeasure2 // - this.buttonMeasure2.Location = new System.Drawing.Point(83, 93); + this.buttonMeasure2.Anchor = System.Windows.Forms.AnchorStyles.Left; + this.buttonMeasure2.Location = new System.Drawing.Point(83, 123); this.buttonMeasure2.Name = "buttonMeasure2"; this.buttonMeasure2.Size = new System.Drawing.Size(80, 24); - this.buttonMeasure2.TabIndex = 26; + this.buttonMeasure2.TabIndex = 8; this.buttonMeasure2.Tag = "2"; this.buttonMeasure2.Text = "Measure"; this.buttonMeasure2.UseVisualStyleBackColor = true; @@ -268,10 +392,10 @@ private void InitializeComponent() // buttonMeasure1 // this.buttonMeasure1.Anchor = System.Windows.Forms.AnchorStyles.Left; - this.buttonMeasure1.Location = new System.Drawing.Point(83, 63); + this.buttonMeasure1.Location = new System.Drawing.Point(83, 93); this.buttonMeasure1.Name = "buttonMeasure1"; this.buttonMeasure1.Size = new System.Drawing.Size(80, 24); - this.buttonMeasure1.TabIndex = 23; + this.buttonMeasure1.TabIndex = 4; this.buttonMeasure1.Tag = "1"; this.buttonMeasure1.Text = "Measure"; this.buttonMeasure1.UseVisualStyleBackColor = true; @@ -279,99 +403,106 @@ private void InitializeComponent() // // labelCoordinate3 // - this.labelCoordinate3.Dock = System.Windows.Forms.DockStyle.Fill; - this.labelCoordinate3.Location = new System.Drawing.Point(3, 120); + this.labelCoordinate3.Location = new System.Drawing.Point(3, 150); this.labelCoordinate3.Name = "labelCoordinate3"; this.labelCoordinate3.Size = new System.Drawing.Size(74, 30); - this.labelCoordinate3.TabIndex = 18; + this.labelCoordinate3.TabIndex = 1000; this.labelCoordinate3.Text = "Coordinate 3:"; this.labelCoordinate3.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; // // labelCoordinate2 // - this.labelCoordinate2.Dock = System.Windows.Forms.DockStyle.Fill; - this.labelCoordinate2.Location = new System.Drawing.Point(3, 90); + this.labelCoordinate2.Location = new System.Drawing.Point(3, 120); this.labelCoordinate2.Name = "labelCoordinate2"; this.labelCoordinate2.Size = new System.Drawing.Size(74, 30); - this.labelCoordinate2.TabIndex = 16; + this.labelCoordinate2.TabIndex = 100; this.labelCoordinate2.Text = "Coordinate 2:"; this.labelCoordinate2.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; // // labelCoordinate1 // - this.labelCoordinate1.Location = new System.Drawing.Point(3, 60); + this.labelCoordinate1.Location = new System.Drawing.Point(3, 90); this.labelCoordinate1.Name = "labelCoordinate1"; this.labelCoordinate1.Size = new System.Drawing.Size(74, 30); - this.labelCoordinate1.TabIndex = 10; + this.labelCoordinate1.TabIndex = 1000; this.labelCoordinate1.Text = "Coordinate 1:"; this.labelCoordinate1.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; // // buttonMeasure0 // - this.buttonMeasure0.Location = new System.Drawing.Point(83, 33); + this.buttonMeasure0.Location = new System.Drawing.Point(83, 63); this.buttonMeasure0.Name = "buttonMeasure0"; this.buttonMeasure0.Size = new System.Drawing.Size(80, 24); - this.buttonMeasure0.TabIndex = 1; + this.buttonMeasure0.TabIndex = 0; this.buttonMeasure0.Tag = "0"; this.buttonMeasure0.Text = "Measure"; this.buttonMeasure0.UseVisualStyleBackColor = true; this.buttonMeasure0.Click += new System.EventHandler(this.ButtonMeasure_Click); // - // labelHeaderTS4231 + // labelTS4231 // - this.tableLayoutPanelCoordinates.SetColumnSpan(this.labelHeaderTS4231, 2); - this.labelHeaderTS4231.Dock = System.Windows.Forms.DockStyle.Fill; - this.labelHeaderTS4231.Location = new System.Drawing.Point(80, 0); - this.labelHeaderTS4231.Margin = new System.Windows.Forms.Padding(0); - this.labelHeaderTS4231.Name = "labelHeaderTS4231"; - this.labelHeaderTS4231.Size = new System.Drawing.Size(302, 30); - this.labelHeaderTS4231.TabIndex = 0; - this.labelHeaderTS4231.Text = "Naive TS4231 Coordinates"; - this.labelHeaderTS4231.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + this.labelTS4231.AutoSize = true; + this.tableLayoutPanelCoordinates.SetColumnSpan(this.labelTS4231, 2); + this.labelTS4231.Dock = System.Windows.Forms.DockStyle.Fill; + this.labelTS4231.Location = new System.Drawing.Point(80, 0); + this.labelTS4231.Margin = new System.Windows.Forms.Padding(0); + this.labelTS4231.Name = "labelTS4231"; + this.labelTS4231.Size = new System.Drawing.Size(266, 30); + this.labelTS4231.TabIndex = 1000; + this.labelTS4231.Text = "TS4231 Coordinates"; + this.labelTS4231.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; // // labelCoordinate0 // - this.labelCoordinate0.Location = new System.Drawing.Point(3, 30); + this.labelCoordinate0.Location = new System.Drawing.Point(3, 60); this.labelCoordinate0.Name = "labelCoordinate0"; this.labelCoordinate0.Size = new System.Drawing.Size(74, 30); - this.labelCoordinate0.TabIndex = 2; + this.labelCoordinate0.TabIndex = 1000; this.labelCoordinate0.Text = "Coordinate 0:"; this.labelCoordinate0.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; // // textBoxTS4231Coordinate0 // - this.textBoxTS4231Coordinate0.Dock = System.Windows.Forms.DockStyle.Fill; - this.textBoxTS4231Coordinate0.Location = new System.Drawing.Point(169, 33); - this.textBoxTS4231Coordinate0.MinimumSize = new System.Drawing.Size(150, 4); + this.textBoxTS4231Coordinate0.Dock = System.Windows.Forms.DockStyle.Bottom; + this.textBoxTS4231Coordinate0.Location = new System.Drawing.Point(169, 67); this.textBoxTS4231Coordinate0.Name = "textBoxTS4231Coordinate0"; this.textBoxTS4231Coordinate0.ReadOnly = true; - this.textBoxTS4231Coordinate0.Size = new System.Drawing.Size(210, 20); - this.textBoxTS4231Coordinate0.TabIndex = 30; + this.textBoxTS4231Coordinate0.Size = new System.Drawing.Size(174, 20); + this.textBoxTS4231Coordinate0.TabIndex = 1000; this.textBoxTS4231Coordinate0.TabStop = false; this.textBoxTS4231Coordinate0.Tag = "0"; // - // labelHeaderUser - // - this.labelHeaderUser.Dock = System.Windows.Forms.DockStyle.Fill; - this.labelHeaderUser.Location = new System.Drawing.Point(385, 0); - this.labelHeaderUser.MinimumSize = new System.Drawing.Size(150, 0); - this.labelHeaderUser.Name = "labelHeaderUser"; - this.labelHeaderUser.Size = new System.Drawing.Size(210, 30); - this.labelHeaderUser.TabIndex = 34; - this.labelHeaderUser.Text = "User-Defined Coordinates"; - this.labelHeaderUser.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; - // - // buttonCalculate - // - this.buttonCalculate.Dock = System.Windows.Forms.DockStyle.Fill; - this.buttonCalculate.Enabled = false; - this.buttonCalculate.Location = new System.Drawing.Point(3, 577); - this.buttonCalculate.Name = "buttonCalculate"; - this.buttonCalculate.Size = new System.Drawing.Size(598, 23); - this.buttonCalculate.TabIndex = 7; - this.buttonCalculate.Text = "Calculate Spatial Transform"; - this.buttonCalculate.UseVisualStyleBackColor = true; - this.buttonCalculate.Click += new System.EventHandler(this.ButtonCalculate_Click); + // textBoxUserCoordinate0X + // + this.textBoxUserCoordinate0X.Dock = System.Windows.Forms.DockStyle.Bottom; + this.textBoxUserCoordinate0X.Location = new System.Drawing.Point(349, 67); + this.textBoxUserCoordinate0X.Name = "textBoxUserCoordinate0X"; + this.textBoxUserCoordinate0X.Size = new System.Drawing.Size(84, 20); + this.textBoxUserCoordinate0X.TabIndex = 1; + this.textBoxUserCoordinate0X.Tag = "0"; + this.textBoxUserCoordinate0X.TextChanged += new System.EventHandler(this.TextBoxUserCoordinate_TextChanged); + // + // labelUser + // + this.tableLayoutPanelCoordinates.SetColumnSpan(this.labelUser, 3); + this.labelUser.Dock = System.Windows.Forms.DockStyle.Fill; + this.labelUser.Location = new System.Drawing.Point(346, 0); + this.labelUser.Margin = new System.Windows.Forms.Padding(0); + this.labelUser.Name = "labelUser"; + this.labelUser.Size = new System.Drawing.Size(272, 30); + this.labelUser.TabIndex = 1000; + this.labelUser.Text = "User Coordinates"; + this.labelUser.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; + // + // labelX + // + this.labelX.Dock = System.Windows.Forms.DockStyle.Fill; + this.labelX.Location = new System.Drawing.Point(349, 30); + this.labelX.Name = "labelX"; + this.labelX.Size = new System.Drawing.Size(84, 30); + this.labelX.TabIndex = 1000; + this.labelX.Text = "X"; + this.labelX.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; // // flowLayoutPanelBottom // @@ -380,81 +511,109 @@ private void InitializeComponent() this.flowLayoutPanelBottom.Controls.Add(this.buttonOK); this.flowLayoutPanelBottom.Dock = System.Windows.Forms.DockStyle.Fill; this.flowLayoutPanelBottom.FlowDirection = System.Windows.Forms.FlowDirection.RightToLeft; - this.flowLayoutPanelBottom.Location = new System.Drawing.Point(3, 606); + this.flowLayoutPanelBottom.Location = new System.Drawing.Point(3, 507); this.flowLayoutPanelBottom.Name = "flowLayoutPanelBottom"; - this.flowLayoutPanelBottom.Size = new System.Drawing.Size(598, 30); - this.flowLayoutPanelBottom.TabIndex = 8; + this.flowLayoutPanelBottom.Size = new System.Drawing.Size(618, 30); + this.flowLayoutPanelBottom.TabIndex = 2; // // buttonCancel // this.buttonCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel; - this.buttonCancel.Location = new System.Drawing.Point(515, 3); + this.buttonCancel.Location = new System.Drawing.Point(535, 3); this.buttonCancel.Name = "buttonCancel"; this.buttonCancel.Size = new System.Drawing.Size(80, 24); - this.buttonCancel.TabIndex = 0; + this.buttonCancel.TabIndex = 1; + this.buttonCancel.Tag = "5"; this.buttonCancel.Text = "Cancel"; this.buttonCancel.UseVisualStyleBackColor = true; - this.buttonCancel.Click += new System.EventHandler(this.ButtonOKOrCancel_Click); // // buttonOK // - this.buttonOK.DialogResult = System.Windows.Forms.DialogResult.OK; - this.buttonOK.Enabled = false; - this.buttonOK.Location = new System.Drawing.Point(429, 3); + this.buttonOK.Location = new System.Drawing.Point(449, 3); this.buttonOK.Name = "buttonOK"; this.buttonOK.Size = new System.Drawing.Size(80, 24); - this.buttonOK.TabIndex = 2; + this.buttonOK.TabIndex = 0; + this.buttonOK.Tag = "4"; this.buttonOK.Text = "OK"; this.buttonOK.UseVisualStyleBackColor = true; - this.buttonOK.Click += new System.EventHandler(this.ButtonOKOrCancel_Click); + this.buttonOK.Click += new System.EventHandler(this.ButtonOK_Click); + // + // tableLayoutPanelSpatialMatrix + // + this.tableLayoutPanelSpatialMatrix.AutoSize = true; + this.tableLayoutPanelSpatialMatrix.ColumnCount = 2; + this.tableLayoutPanelSpatialMatrix.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); + this.tableLayoutPanelSpatialMatrix.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); + this.tableLayoutPanelSpatialMatrix.Controls.Add(this.labelSpatialMatrix, 0, 0); + this.tableLayoutPanelSpatialMatrix.Controls.Add(this.textBoxSpatialTransformMatrix, 1, 0); + this.tableLayoutPanelSpatialMatrix.Dock = System.Windows.Forms.DockStyle.Fill; + this.tableLayoutPanelSpatialMatrix.Location = new System.Drawing.Point(3, 306); + this.tableLayoutPanelSpatialMatrix.Name = "tableLayoutPanelSpatialMatrix"; + this.tableLayoutPanelSpatialMatrix.RowCount = 1; + this.tableLayoutPanelSpatialMatrix.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 60F)); + this.tableLayoutPanelSpatialMatrix.Size = new System.Drawing.Size(618, 60); + this.tableLayoutPanelSpatialMatrix.TabIndex = 0; + // + // labelSpatialMatrix + // + this.labelSpatialMatrix.AutoSize = true; + this.labelSpatialMatrix.Dock = System.Windows.Forms.DockStyle.Fill; + this.labelSpatialMatrix.Location = new System.Drawing.Point(3, 0); + this.labelSpatialMatrix.Name = "labelSpatialMatrix"; + this.labelSpatialMatrix.Size = new System.Drawing.Size(123, 60); + this.labelSpatialMatrix.TabIndex = 1000; + this.labelSpatialMatrix.Text = "Spatial Transform Matrix:"; + // + // textBoxSpatialTransformMatrix + // + this.textBoxSpatialTransformMatrix.AcceptsReturn = true; + this.textBoxSpatialTransformMatrix.AcceptsTab = true; + this.textBoxSpatialTransformMatrix.Dock = System.Windows.Forms.DockStyle.Fill; + this.textBoxSpatialTransformMatrix.Location = new System.Drawing.Point(132, 3); + this.textBoxSpatialTransformMatrix.Multiline = true; + this.textBoxSpatialTransformMatrix.Name = "textBoxSpatialTransformMatrix"; + this.textBoxSpatialTransformMatrix.ReadOnly = true; + this.textBoxSpatialTransformMatrix.Size = new System.Drawing.Size(483, 54); + this.textBoxSpatialTransformMatrix.TabIndex = 1000; + this.textBoxSpatialTransformMatrix.TabStop = false; // // statusStrip // this.statusStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { - this.toolStripStatusLabelTS4231, - this.toolStripStatusLabelUser}); - this.statusStrip.Location = new System.Drawing.Point(0, 639); + this.toolStripStatusLabel}); + this.statusStrip.LayoutStyle = System.Windows.Forms.ToolStripLayoutStyle.Flow; + this.statusStrip.Location = new System.Drawing.Point(0, 540); this.statusStrip.Name = "statusStrip"; this.statusStrip.ShowItemToolTips = true; - this.statusStrip.Size = new System.Drawing.Size(604, 22); - this.statusStrip.TabIndex = 8; - this.statusStrip.Text = "statusStrip1"; + this.statusStrip.Size = new System.Drawing.Size(624, 21); + this.statusStrip.TabIndex = 1000; // - // toolStripStatusLabelTS4231 + // toolStripStatusLabel // - this.toolStripStatusLabelTS4231.Image = global::OpenEphys.Onix1.Design.Properties.Resources.StatusBlockedImage; - this.toolStripStatusLabelTS4231.Name = "toolStripStatusLabelTS4231"; - this.toolStripStatusLabelTS4231.Size = new System.Drawing.Size(237, 17); - this.toolStripStatusLabelTS4231.Text = "At least one TS4231 coordinate is invalid."; - this.toolStripStatusLabelTS4231.ToolTipText = "All four TS4231 coordinates must be measured before the spatial transform matrix " + - "can be calculated."; - // - // toolStripStatusLabelUser - // - this.toolStripStatusLabelUser.Image = global::OpenEphys.Onix1.Design.Properties.Resources.StatusBlockedImage; - this.toolStripStatusLabelUser.Name = "toolStripStatusLabelUser"; - this.toolStripStatusLabelUser.Size = new System.Drawing.Size(267, 17); - this.toolStripStatusLabelUser.Text = "At least one user-defined coordinate is invalid."; - this.toolStripStatusLabelUser.ToolTipText = resources.GetString("toolStripStatusLabelUser.ToolTipText"); + this.toolStripStatusLabel.Image = global::OpenEphys.Onix1.Design.Properties.Resources.StatusWarningImage; + this.toolStripStatusLabel.Name = "toolStripStatusLabel"; + this.toolStripStatusLabel.Size = new System.Drawing.Size(221, 16); + this.toolStripStatusLabel.Text = "All fields must be properly populated."; // // SpatialTransformMatrixDialog // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(604, 661); + this.ClientSize = new System.Drawing.Size(624, 561); this.Controls.Add(this.tableLayoutPanelMain); this.Controls.Add(this.statusStrip); this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); - this.MinimumSize = new System.Drawing.Size(620, 700); + this.MinimumSize = new System.Drawing.Size(640, 600); this.Name = "SpatialTransformMatrixDialog"; this.Text = "TS4231V1 Calibration GUI"; this.tableLayoutPanelMain.ResumeLayout(false); this.tableLayoutPanelMain.PerformLayout(); this.groupBoxStatus.ResumeLayout(false); - this.groupBoxStatus.PerformLayout(); this.tableLayoutPanelCoordinates.ResumeLayout(false); this.tableLayoutPanelCoordinates.PerformLayout(); this.flowLayoutPanelBottom.ResumeLayout(false); + this.tableLayoutPanelSpatialMatrix.ResumeLayout(false); + this.tableLayoutPanelSpatialMatrix.PerformLayout(); this.statusStrip.ResumeLayout(false); this.statusStrip.PerformLayout(); this.ResumeLayout(false); @@ -466,33 +625,46 @@ private void InitializeComponent() private System.Windows.Forms.TableLayoutPanel tableLayoutPanelMain; private System.Windows.Forms.GroupBox groupBoxStatus; - private System.Windows.Forms.TextBox textBoxStatus; private System.Windows.Forms.TableLayoutPanel tableLayoutPanelCoordinates; - private System.Windows.Forms.TextBox textBoxUserCoordinate3; - private System.Windows.Forms.TextBox textBoxUserCoordinate2; - private System.Windows.Forms.TextBox textBoxUserCoordinate1; - private System.Windows.Forms.TextBox textBoxUserCoordinate0; - private System.Windows.Forms.TextBox textBoxTS4231Coordinate3; private System.Windows.Forms.TextBox textBoxTS4231Coordinate2; private System.Windows.Forms.TextBox textBoxTS4231Coordinate1; - private System.Windows.Forms.Button buttonMeasure3; private System.Windows.Forms.Button buttonMeasure2; private System.Windows.Forms.Button buttonMeasure1; - private System.Windows.Forms.Label labelCoordinate3; private System.Windows.Forms.Label labelCoordinate2; private System.Windows.Forms.Label labelCoordinate1; private System.Windows.Forms.Button buttonMeasure0; - private System.Windows.Forms.Label labelHeaderTS4231; + private System.Windows.Forms.Label labelTS4231; private System.Windows.Forms.Label labelCoordinate0; private System.Windows.Forms.TextBox textBoxTS4231Coordinate0; - private System.Windows.Forms.Label labelHeaderUser; - private System.Windows.Forms.Button buttonCalculate; private System.Windows.Forms.FlowLayoutPanel flowLayoutPanelBottom; private System.Windows.Forms.Button buttonCancel; private System.Windows.Forms.Label labelInstructions; private System.Windows.Forms.Button buttonOK; private System.Windows.Forms.StatusStrip statusStrip; - private System.Windows.Forms.ToolStripStatusLabel toolStripStatusLabelUser; - private System.Windows.Forms.ToolStripStatusLabel toolStripStatusLabelTS4231; + private System.Windows.Forms.ToolStripStatusLabel toolStripStatusLabel; + private System.Windows.Forms.TextBox textBoxTS4231Coordinate3; + private System.Windows.Forms.Button buttonMeasure3; + private System.Windows.Forms.Label labelCoordinate3; + private System.Windows.Forms.Label labelXyz; + private System.Windows.Forms.TextBox textBoxUserCoordinate0X; + private System.Windows.Forms.Label labelUser; + private System.Windows.Forms.TextBox textBoxUserCoordinate3Z; + private System.Windows.Forms.TextBox textBoxUserCoordinate3Y; + private System.Windows.Forms.TextBox textBoxUserCoordinate3X; + private System.Windows.Forms.TextBox textBoxUserCoordinate2Z; + private System.Windows.Forms.TextBox textBoxUserCoordinate2Y; + private System.Windows.Forms.TextBox textBoxUserCoordinate2X; + private System.Windows.Forms.TextBox textBoxUserCoordinate1Z; + private System.Windows.Forms.TextBox textBoxUserCoordinate1Y; + private System.Windows.Forms.TextBox textBoxUserCoordinate0Z; + private System.Windows.Forms.TextBox textBoxUserCoordinate1X; + private System.Windows.Forms.Label labelY; + private System.Windows.Forms.Label labelX; + private System.Windows.Forms.Label labelZ; + private System.Windows.Forms.TableLayoutPanel tableLayoutPanelSpatialMatrix; + private System.Windows.Forms.Label labelSpatialMatrix; + private System.Windows.Forms.TextBox textBoxSpatialTransformMatrix; + private System.Windows.Forms.RichTextBox richTextBoxStatus; + private System.Windows.Forms.TextBox textBoxUserCoordinate0Y; } } diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs index 793375eb..e93b1dc3 100644 --- a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs +++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs @@ -1,74 +1,73 @@ using System; +using System.Drawing; using System.Linq; using System.Numerics; -using System.Windows.Forms; using System.Reactive.Linq; +using System.Windows.Forms; using Bonsai.Design; namespace OpenEphys.Onix1.Design { /// - /// Partial class to create a spatial-calibration GUI for . + /// Partial class to create a spatial-calibration GUI for . /// public partial class SpatialTransformMatrixDialog : Form { + internal SpatialTransform3D SpatialTransform; const byte NumMeasurements = 100; - readonly Matrix4x4 inverseM; - - readonly bool[] InputsValid = { false, false, false, false, false, false, false, false }; readonly IObservable PositionDataSource; IDisposable richTextBoxStatusUpdateSubscription; IDisposable MeasurementCalculationSubscription; - internal Matrix4x4 NewSpatialTransform { get; private set; } - - internal SpatialTransformMatrixDialog(IObservable positionDataSource, Matrix4x4 currentM) + internal SpatialTransformMatrixDialog(IObservable dataSource, SpatialTransform3D transformProperties) { InitializeComponent(); SpatialTransform = transformProperties; PositionDataSource = dataSource; - var ts4231TextBoxes = new TextBox[] { - textBoxTS4231Coordinate0, textBoxTS4231Coordinate1, - textBoxTS4231Coordinate2, textBoxTS4231Coordinate3}; - foreach (var (textBox, v) in Enumerable.Zip(ts4231TextBoxes, SpatialTransform.Pre, (tb, v) => (tb, v))) - textBox.Text = checkVector3ForNaN(v) ? "" : $"{v.X}, {v.Y}, {v.Z}"; + var ts4231TextBoxes = new TextBox[] { + textBoxTS4231Coordinate0, textBoxTS4231Coordinate1, + textBoxTS4231Coordinate2, textBoxTS4231Coordinate3 }; + var preTransformCoordinates = MatrixToFloatArray(SpatialTransform.A); + for (byte i = 0; i < 4; i++) + ts4231TextBoxes[i].Text = float.IsNaN(preTransformCoordinates[i * 3]) ? "" : $"{preTransformCoordinates[i * 3]}, " + + $"{preTransformCoordinates[i * 3 + 1]}, " + + $"{preTransformCoordinates[i * 3 + 2]}"; var userTextBoxes = new TextBox[] { textBoxUserCoordinate0X, textBoxUserCoordinate0Y, textBoxUserCoordinate0Z, textBoxUserCoordinate1X, textBoxUserCoordinate1Y, textBoxUserCoordinate1Z, textBoxUserCoordinate2X, textBoxUserCoordinate2Y, textBoxUserCoordinate2Z, - textBoxUserCoordinate3X, textBoxUserCoordinate3Y, textBoxUserCoordinate3Z}; - for (byte i = 0; i < 12; i++) - { - ref var component = ref GetComponent(ref SpatialTransform.Post[i / 3], i % 3); - userTextBoxes[i].Text = float.IsNaN(component) ? "" : component.ToString(); - } + textBoxUserCoordinate3X, textBoxUserCoordinate3Y, textBoxUserCoordinate3Z }; + var postTransformCoordinates = MatrixToFloatArray(SpatialTransform.B); + foreach (var (tb, comp) in Enumerable.Zip(userTextBoxes, postTransformCoordinates, (tb, comp) => (tb, comp))) + tb.Text = float.IsNaN(comp) ? "" : comp.ToString(); - CalculatePrintMatrix(); + IndicateSpatialTransformStatus(); } - private void TextBoxUserCoordinate_TextChanged(object sender, EventArgs e) + void TextBoxUserCoordinate_TextChanged(object sender, EventArgs e) { var tag = Convert.ToByte(((TextBox)sender).Tag); - ref var coordinateComponent = ref GetComponent(ref SpatialTransform.Post[tag / 3], tag % 3); - try { coordinateComponent = float.Parse(((TextBox)sender).Text); } - catch { coordinateComponent = float.NaN; } - CalculatePrintMatrix(); + try { SpatialTransform.B = SetMatrixElement(SpatialTransform.B, float.Parse(((TextBox)sender).Text), tag / 3, tag % 3); } + catch { SpatialTransform.B = SetMatrixElement(SpatialTransform.B, float.NaN, tag / 3, tag % 3); } + IndicateSpatialTransformStatus(); } - private void ButtonMeasure_Click(object sender, EventArgs e) + void ButtonMeasure_Click(object sender, EventArgs e) { TextBox[] ts4231TextBoxes = { textBoxTS4231Coordinate0, textBoxTS4231Coordinate1, textBoxTS4231Coordinate2, textBoxTS4231Coordinate3 }; var index = Convert.ToByte(((Button)sender).Tag); + + for (byte i = 0; i < 3; i++) + SpatialTransform.A = SetMatrixElement(SpatialTransform.A, float.NaN, index, i); ts4231TextBoxes[index].Text = ""; - SpatialTransform.Pre[index] = new(float.NaN); + if (((Button)sender).Text == "Measure") { richTextBoxStatus.SelectionColor = Color.Blue; richTextBoxStatus.AppendText($"Measurement at coordinate {index} initiated.\n"); - SpatialTransform.M = null; - textBoxSpatialTransformMatrix.Text = ""; + IndicateSpatialTransformStatus(); ((Button)sender).Text = "Cancel"; EnableButtons(false, index); @@ -77,34 +76,30 @@ private void ButtonMeasure_Click(object sender, EventArgs e) .Timeout(new TimeSpan(0, 0, 5), Observable.Empty()) .Publish(); - TextBoxStatusUpdateSubscription = sharedPositionDataGroups + richTextBoxStatusUpdateSubscription = sharedPositionDataGroups .GroupBy(dataFrame => dataFrame.SensorIndex, dataFrame => dataFrame.Position) .SelectMany(group => group.Count().Select(count => new { Index = group.Key, MeasurementCount = count })) .Aggregate( - (TextBoxStatusUpdate: "", Count: 0), + (richTextBoxStatusUpdate: "", Count: 0), (acc, sensor) => { - var textBoxStatusUpdateString = acc.TextBoxStatusUpdate; - textBoxStatusUpdateString += string.Format("{0} measurements from sensor {1}.", - sensor.MeasurementCount, sensor.Index); - textBoxStatusUpdateString += Environment.NewLine; - return (textBoxStatusUpdateString, acc.Count + sensor.MeasurementCount); + var richTextBoxStatusUpdateString = $"{acc.richTextBoxStatusUpdate}{sensor.MeasurementCount} samples from sensor {sensor.Index}.\n"; + return (richTextBoxStatusUpdateString, acc.Count + sensor.MeasurementCount); }, - acc => (acc.TextBoxStatusUpdate, Valid: acc.Count == NumMeasurements)) + acc => (acc.richTextBoxStatusUpdate, Valid: acc.Count == NumMeasurements)) .ObserveOn(new ControlScheduler(this)) .Subscribe(finalResult => { if (finalResult.Valid) { - textBoxStatus.AppendText(finalResult.TextBoxStatusUpdate); - textBoxStatus.AppendText(string.Format("Measurements at coordinate {0} complete.", index) - + Environment.NewLine + Environment.NewLine + "Awaiting user input..." + Environment.NewLine); + richTextBoxStatus.SelectionColor = Color.Black; + richTextBoxStatus.AppendText($"{finalResult.richTextBoxStatusUpdate}Measurement at coordinate {index} complete.\n\n"); } else { - textBoxStatus.AppendText(string.Format("Measurements at coordinate {0} timed out. ", index) - + "Confirm the Lighthouse receivers are within range and unobstructed from Lighthouse transmitters." - + Environment.NewLine + Environment.NewLine + "Awaiting user input..." + Environment.NewLine); + richTextBoxStatus.SelectionColor = Color.Red; + richTextBoxStatus.AppendText($"Measurement at coordinate {index} timed out.\n" + + "Confirm the Lighthouse receivers are within range of and unobstructed from Lighthouse transmitters.\n\n"); } EnableButtons(true, index); }); @@ -115,31 +110,20 @@ private void ButtonMeasure_Click(object sender, EventArgs e) (acc, current) => (acc.Sum + current.Position, acc.Count + 1), acc => { - SpatialTransform.Pre[index] = acc.Sum / NumMeasurements; - return (Position: SpatialTransform.Pre[index], Valid: acc.Count == NumMeasurements); + var measurement = acc.Sum / NumMeasurements; + SpatialTransform.A = SetMatrixElement(SpatialTransform.A, measurement.X, index, 0); + SpatialTransform.A = SetMatrixElement(SpatialTransform.A, measurement.Y, index, 1); + SpatialTransform.A = SetMatrixElement(SpatialTransform.A, measurement.Z, index, 2); + return (Position: measurement, Valid: acc.Count == NumMeasurements); }) .ObserveOn(new ControlScheduler(this)) - .Subscribe(finalMeasurement => + .Subscribe(measurement => { - MeasureButtons[index].Text = "Measure"; - if (finalMeasurement.Valid) + ((Button)sender).Text = "Measure"; + if (measurement.Valid) { - ts4231TextBoxes[index].Text = string.Format("{0}, {1}, {2}", - finalMeasurement.Position.X, - finalMeasurement.Position.Y, - finalMeasurement.Position.Z); - InputsValid[index] = true; - if (InputsValid.Take(4).All(ts4231InputValid => ts4231InputValid)) - { - toolStripStatusLabelTS4231.Image = OpenEphys.Onix1.Design.Properties.Resources.StatusReadyImage; - toolStripStatusLabelTS4231.Text = "All TS4231 coordinates are valid."; - } - else - { - toolStripStatusLabelTS4231.Image = OpenEphys.Onix1.Design.Properties.Resources.StatusBlockedImage; - toolStripStatusLabelTS4231.Text = "At least one TS4231 coordinate is invalid."; - } - buttonCalculate.Enabled = InputsValid.All(inputValid => inputValid); + ts4231TextBoxes[index].Text = $"{measurement.Position.X}, {measurement.Position.Y}, {measurement.Position.Z}"; + IndicateSpatialTransformStatus(); } }); @@ -147,132 +131,111 @@ private void ButtonMeasure_Click(object sender, EventArgs e) } else { - TextBoxStatusUpdateSubscription.Dispose(); + richTextBoxStatusUpdateSubscription.Dispose(); MeasurementCalculationSubscription.Dispose(); - textBoxStatus.AppendText(string.Format("Measurements at coordinate {0} cancelled by user.", index) - + Environment.NewLine + Environment.NewLine + "Awaiting user input..." + Environment.NewLine); - MeasureButtons[index].Text = "Measure"; + richTextBoxStatus.SelectionColor = Color.Red; + richTextBoxStatus.AppendText($"Measurement at coordinate {index} cancelled by user.\n\n"); + ((Button)sender).Text = "Measure"; EnableButtons(true, index); } } - private void ButtonOK_Click(object sender, EventArgs e) + void ButtonOK_Click(object sender, EventArgs e) { - if (SpatialTransform.M.HasValue) - DialogResult = DialogResult.OK; - else + var confirmationMessage = ""; + var invalidInput = false; + if (ContainsNaN(SpatialTransform.A) || ContainsNaN(SpatialTransform.B)) { - var confirmationMessage = ""; - var incompleteInput = false; - if (SpatialTransform.Post.Any(userCoordinate => checkVector3ForNaN(userCoordinate))) - { - incompleteInput = true; - var axes = new char[] { 'X', 'Y', 'Z' }; - var coordinates = new byte[] { 0, 1, 2, 3 }; - confirmationMessage += "At least one coordinate component is empty or invalid:\n"; - for (byte i = 0; i < 12; i++) - { - ref var component = ref GetComponent(ref SpatialTransform.Post[i / 3], i % 3); - if (float.IsNaN(component)) - confirmationMessage += $" • Coordinate {coordinates[i / 3]} {axes[i % 3]} component\n"; - } - confirmationMessage += "\n"; - } - if (SpatialTransform.Pre.Any(TS4231Coordinate => checkVector3ForNaN(TS4231Coordinate))) - { - incompleteInput = true; - confirmationMessage += "At least one coordinate measurement is empty:\n"; - foreach (var (i, v) in SpatialTransform.Pre.Select((i, v) => (v, i))) - if (checkVector3ForNaN(v)) - confirmationMessage += $" • Coordinate {i}\n"; - confirmationMessage += "\n"; - } + confirmationMessage = $"At least one entry in the TS4231V1 Calibration GUI form is invalid:\n\n"; + + for (byte i = 0; i < 4; i++) + if (float.IsNaN(MatrixToFloatArray(SpatialTransform.A)[i * 3])) + confirmationMessage += $" • TS4231 coordinate {i}\n"; - if (incompleteInput) - confirmationMessage += "They will not be saved and transformed position data won't be properly output.\n\n"; - else if (!Matrix4x4.Invert(Vector3sToMatrix4x4(SpatialTransform.Post), out _)) - confirmationMessage = "The spatial transform matrix is non-invertible. The transformed position data won't be properly output.\n\n"; + var axes = new char[] { 'X', 'Y', 'Z' }; + var coordinates = new byte[] { 0, 1, 2, 3 }; - confirmationMessage += "Would you like to continue?"; + for (byte i = 0; i < 12; i++) + if (float.IsNaN(MatrixToFloatArray(SpatialTransform.B)[i])) + confirmationMessage += $" • Component {axes[i % 3]} from user coordinate {coordinates[i / 3]}\n"; + confirmationMessage += "\nAny invalid entry will not be saved. "; + invalidInput = true; + } + else if (!Matrix4x4.Invert(SpatialTransform.M, out _)) + { + confirmationMessage = $"The calculated spatial transform matrix is non-invertible. "; + invalidInput = true; + } + + if (invalidInput) + { + confirmationMessage += "The transformed position data will be NaNs until all entries are valid.\n\n" + + "Would you like to continue?"; if (MessageBox.Show(confirmationMessage, "Confirmation", MessageBoxButtons.YesNo) == DialogResult.Yes) DialogResult = DialogResult.OK; - } + } + else + DialogResult = DialogResult.OK; } - private readonly Func checkVector3ForNaN = v => new[] { v.X, v.Y, v.Z }.Any(float.IsNaN); - - private void EnableButtons(bool enable, byte index) + void EnableButtons(bool enable, byte index) { var buttons = new Button[] { buttonMeasure0, buttonMeasure1, buttonMeasure2, buttonMeasure3, buttonOK, buttonCancel }; Array.ForEach(buttons, button => button.Enabled = enable || (Convert.ToByte(button.Tag) == index)); } - private void CalculatePrintMatrix() + void IndicateSpatialTransformStatus() { - SpatialTransform.M = null; - if (!SpatialTransform.Post.Any(userCoordinate => checkVector3ForNaN(userCoordinate)) && - !SpatialTransform.Pre.Any(TS4231Coordinate => checkVector3ForNaN(TS4231Coordinate))) + if (ContainsNaN(SpatialTransform.A) || ContainsNaN(SpatialTransform.B)) { - if (Matrix4x4.Invert(Vector3sToMatrix4x4(SpatialTransform.Post), out _)) - { - var ts4231V1CoordinatesMatrix = Vector3sToMatrix4x4(SpatialTransform.Pre); - var userCoordinatesMatrix = Vector3sToMatrix4x4(SpatialTransform.Post); - Matrix4x4.Invert(ts4231V1CoordinatesMatrix, out var ts4231V1CoordinatesMatrixInverted); - SpatialTransform.M = Matrix4x4.Multiply(ts4231V1CoordinatesMatrixInverted, userCoordinatesMatrix); - toolStripStatusLabel.Image = Properties.Resources.StatusReadyImage; - toolStripStatusLabel.Text = "Spatial transform matrix is calculated."; - } - else - { - toolStripStatusLabel.Image = Properties.Resources.StatusWarningImage; - toolStripStatusLabel.Text = "The resulting spatial transform matrix must be non-invertible."; - } + toolStripStatusLabel.Image = Properties.Resources.StatusWarningImage; + toolStripStatusLabel.Text = "All fields must be properly populated."; + textBoxSpatialTransformMatrix.Text = ""; } - else + else if (!Matrix4x4.Invert(SpatialTransform.M, out _)) { - toolStripStatusLabelUser.Image = OpenEphys.Onix1.Design.Properties.Resources.StatusBlockedImage; - toolStripStatusLabelUser.Text = "At least one user-defined coordinate is invalid."; - } - if (SpatialTransform.M.HasValue) - textBoxSpatialTransformMatrix.Text = SpatialTransform.M.Value.ToString(); - else + toolStripStatusLabel.Image = Properties.Resources.StatusWarningImage; + toolStripStatusLabel.Text = "The calculated spatial transform matrix must be invertible."; textBoxSpatialTransformMatrix.Text = ""; + } + else + { + toolStripStatusLabel.Image = Properties.Resources.StatusReadyImage; + toolStripStatusLabel.Text = "Spatial transform matrix is calculated."; + textBoxSpatialTransformMatrix.Text = SpatialTransform.M.ToString(); + } } - private void ButtonCalculate_Click(object sender, EventArgs e) - { - var ts4231V1CoordinatesMatrix = new Matrix4x4( - TS4231Coordinates[0].X, TS4231Coordinates[0].Y, TS4231Coordinates[0].Z, 1, - TS4231Coordinates[1].X, TS4231Coordinates[1].Y, TS4231Coordinates[1].Z, 1, - TS4231Coordinates[2].X, TS4231Coordinates[2].Y, TS4231Coordinates[2].Z, 1, - TS4231Coordinates[3].X, TS4231Coordinates[3].Y, TS4231Coordinates[3].Z, 1); - - float[][] userCoordinates = { - textBoxUserCoordinate0.Text.Split(',').Select(item => float.Parse(item)).ToArray(), - textBoxUserCoordinate1.Text.Split(',').Select(item => float.Parse(item)).ToArray(), - textBoxUserCoordinate2.Text.Split(',').Select(item => float.Parse(item)).ToArray(), - textBoxUserCoordinate3.Text.Split(',').Select(item => float.Parse(item)).ToArray()}; - - var userCoordinatesMatrix = new Matrix4x4( - userCoordinates[0][0], userCoordinates[0][1], userCoordinates[0][2], 1, - userCoordinates[1][0], userCoordinates[1][1], userCoordinates[1][2], 1, - userCoordinates[2][0], userCoordinates[2][1], userCoordinates[2][2], 1, - userCoordinates[3][0], userCoordinates[3][1], userCoordinates[3][2], 1); + static float[] MatrixToFloatArray(Matrix4x4 m) => + new float[] { m.M11, m.M12, m.M13, + m.M21, m.M22, m.M23, + m.M31, m.M32, m.M33, + m.M41, m.M42, m.M43 }; - Matrix4x4.Invert(ts4231V1CoordinatesMatrix, out var ts4231V1CoordinatesMatrixInverted); - NewSpatialTransform = Matrix4x4.Multiply(ts4231V1CoordinatesMatrixInverted, userCoordinatesMatrix); + static bool ContainsNaN(Matrix4x4 m) => MatrixToFloatArray(m).Any(float.IsNaN); - textBoxStatus.AppendText("The spatial transform matrix for the above coordinates is:" + Environment.NewLine); - textBoxStatus.AppendText(NewSpatialTransform.ToString() + Environment.NewLine + Environment.NewLine); - textBoxStatus.AppendText("Awaiting user input..." + Environment.NewLine); - - buttonOK.Enabled = true; - } - - private void ButtonOKOrCancel_Click(object sender, EventArgs e) + static Matrix4x4 SetMatrixElement(Matrix4x4 m, float value, int coordinate, int component) { - Close(); + if (coordinate is < 0 or > 3) throw new ArgumentOutOfRangeException(nameof(coordinate) + " must be 0, 1, 2, or 3."); + if (component is < 0 or > 2) throw new ArgumentOutOfRangeException(nameof(component) + " must be 0, 1, or 2."); + + switch ((coordinate, component)) + { + case (0, 0): m.M11 = value; break; + case (0, 1): m.M12 = value; break; + case (0, 2): m.M13 = value; break; + case (1, 0): m.M21 = value; break; + case (1, 1): m.M22 = value; break; + case (1, 2): m.M23 = value; break; + case (2, 0): m.M31 = value; break; + case (2, 1): m.M32 = value; break; + case (2, 2): m.M33 = value; break; + case (3, 0): m.M41 = value; break; + case (3, 1): m.M42 = value; break; + case (3, 2): m.M43 = value; break; + } + return m; } } } diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.resx b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.resx index f9416718..fabd65ad 100644 --- a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.resx +++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.resx @@ -118,21 +118,21 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Follow the instructions below to transform naive TS4231 position data from the base-station reference frame to a user-defined reference frame. For more in-depth instructions, find the corresponding tutorial in Open Ephys' online documentation. -1) For each coordinate: - • Place the TS4231V1 device and click the corresponding "Measure" button. - • Input how you would like to define the coordinate in the user-defined reference frame. -2) Click the "Calculate Spatial Transform" button which enables after all fields are populated with valid data. -3) Click "OK" to close this GUI and set the M property as the calculated spatial transform matrix. Click "Cancel" to close this GUI and not set the M property. + Follow the instructions below to transform TS4231 position data from a generic base-station reference frame to a user-defined reference frame: + +1) For each coordinate: + • Place the TS4231V1 device and click the "Measure" button. + • Define the coordinate in the user-defined reference frame using the fields under "User Coordinates". +2) Click "OK" to close this GUI and set the spatial transform properties in the workflow. + +For more in-depth instructions, find the corresponding tutorial in Open Ephys' online documentation. - 36, 14 + -2, 3 + + + 81 - - All four user-defined coordinates must have the following format: "XX, YY, ZZ" or -"XX.XX, YY.YY, ZZ.ZZ" with any number of digits following the decimal before the -spatial transform matrix can be calculated. - diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs b/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs index 3a4bd790..6be7ac3e 100644 --- a/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs +++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs @@ -5,10 +5,8 @@ using System.Windows.Forms.Design; using System.Windows.Forms; using System.Reactive.Linq; -using System.Numerics; using Bonsai.Design; - namespace OpenEphys.Onix1.Design { /// @@ -18,10 +16,7 @@ namespace OpenEphys.Onix1.Design public class SpatialTransformMatrixEditor : DataSourceTypeEditor { /// - public SpatialTransformMatrixEditor() - : base(DataSource.Output, typeof(void)) - { - } + public SpatialTransformMatrixEditor() : base(DataSource.Input, typeof(void)) { } /// public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context) @@ -38,14 +33,14 @@ public override object EditValue(ITypeDescriptorContext context, IServiceProvide { var source = GetDataSource(context, provider); var dataFrames = source.Output.Merge().Select(x => x as TS4231V1PositionDataFrame); - using var visualizerDialog = new SpatialTransformMatrixDialog(dataFrames, new SpatialTransformProperties((SpatialTransformProperties)value)); + using var visualizerDialog = new SpatialTransformMatrixDialog(dataFrames, new SpatialTransform3D((SpatialTransform3D)value)); if (!editorState.WorkflowRunning) { throw new InvalidOperationException("Workflow must be running to open this GUI."); } else if (editorService.ShowDialog(visualizerDialog) == DialogResult.OK) { - return visualizerDialog.NewSpatialTransform; + return visualizerDialog.SpatialTransform; } } return base.EditValue(context, provider, value); From 096f9ba907f693afd0d5064361c78d87905eb1ff Mon Sep 17 00:00:00 2001 From: cjsha Date: Sun, 24 Aug 2025 22:49:23 -0400 Subject: [PATCH 16/17] fix recent changes --- .bonsai/Bonsai.config | 5 - .../debug/spatial-transform-test.editor | 37 -- .../debug/spatial-transform-test.layout | 22 - .../SpatialTransformMatrixDialog.cs | 67 +- OpenEphys.Onix1/OpenEphys.Onix1.csproj | 4 - OpenEphys.Onix1/SpatialTransform3D.cs | 77 +-- .../TS4231V1TransformedPositionData.bonsai | 89 --- spatial-transform_issue-427.patch | 599 ------------------ 8 files changed, 67 insertions(+), 833 deletions(-) delete mode 100644 .bonsai/Settings/artifacts/bin/OpenEphys.Onix1.Design/debug/spatial-transform-test.editor delete mode 100644 .bonsai/Settings/artifacts/bin/OpenEphys.Onix1.Design/debug/spatial-transform-test.layout delete mode 100644 OpenEphys.Onix1/TS4231V1TransformedPositionData.bonsai delete mode 100644 spatial-transform_issue-427.patch diff --git a/.bonsai/Bonsai.config b/.bonsai/Bonsai.config index e0dcade2..5a4a1e2a 100644 --- a/.bonsai/Bonsai.config +++ b/.bonsai/Bonsai.config @@ -48,19 +48,14 @@ - - - - - diff --git a/.bonsai/Settings/artifacts/bin/OpenEphys.Onix1.Design/debug/spatial-transform-test.editor b/.bonsai/Settings/artifacts/bin/OpenEphys.Onix1.Design/debug/spatial-transform-test.editor deleted file mode 100644 index bbd6dca8..00000000 --- a/.bonsai/Settings/artifacts/bin/OpenEphys.Onix1.Design/debug/spatial-transform-test.editor +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.bonsai/Settings/artifacts/bin/OpenEphys.Onix1.Design/debug/spatial-transform-test.layout b/.bonsai/Settings/artifacts/bin/OpenEphys.Onix1.Design/debug/spatial-transform-test.layout deleted file mode 100644 index ad8bce08..00000000 --- a/.bonsai/Settings/artifacts/bin/OpenEphys.Onix1.Design/debug/spatial-transform-test.layout +++ /dev/null @@ -1,22 +0,0 @@ - - - - - 26 - 26 - - - 416 - 279 - - OpenEphys.Onix1.Design.Vector3Visualizer - - - 640 - 0 - 1.1 - true - - - - \ No newline at end of file diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs index 7f96c594..c936bdcd 100644 --- a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs +++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs @@ -39,7 +39,7 @@ internal SpatialTransformMatrixDialog(IObservable dat var ts4231TextBoxes = new TextBox[] { textBoxTS4231Coordinate0, textBoxTS4231Coordinate1, textBoxTS4231Coordinate2, textBoxTS4231Coordinate3 }; - var preTransformCoordinates = SpatialTransform.MatrixToFloatArray(SpatialTransform.A); + var preTransformCoordinates = MatrixToFloatArray(SpatialTransform.A); for (byte i = 0; i < 4; i++) ts4231TextBoxes[i].Text = float.IsNaN(preTransformCoordinates[i * 3]) ? "" : $"{preTransformCoordinates[i * 3]}, " + $"{preTransformCoordinates[i * 3 + 1]}, " + @@ -50,7 +50,7 @@ internal SpatialTransformMatrixDialog(IObservable dat textBoxUserCoordinate1X, textBoxUserCoordinate1Y, textBoxUserCoordinate1Z, textBoxUserCoordinate2X, textBoxUserCoordinate2Y, textBoxUserCoordinate2Z, textBoxUserCoordinate3X, textBoxUserCoordinate3Y, textBoxUserCoordinate3Z }; - var postTransformCoordinates = SpatialTransform.MatrixToFloatArray(SpatialTransform.B); + var postTransformCoordinates = MatrixToFloatArray(SpatialTransform.B); foreach (var (tb, comp) in Enumerable.Zip(userTextBoxes, postTransformCoordinates, (tb, comp) => (tb, comp))) tb.Text = float.IsNaN(comp) ? "" : comp.ToString(); @@ -60,8 +60,8 @@ internal SpatialTransformMatrixDialog(IObservable dat void TextBoxUserCoordinate_TextChanged(object sender, EventArgs e) { var tag = Convert.ToByte(((TextBox)sender).Tag); - try { SpatialTransform.SetMatrixBElement(float.Parse(((TextBox)sender).Text), tag / 3, tag % 3); } - catch { SpatialTransform.SetMatrixBElement(float.NaN, tag / 3, tag % 3); } + try { SpatialTransform.B = SetMatrixElement(SpatialTransform.B, float.Parse(((TextBox)sender).Text), tag / 3, tag % 3); } + catch { SpatialTransform.B = SetMatrixElement(SpatialTransform.B, float.NaN, tag / 3, tag % 3); } IndicateSpatialTransformStatus(); } @@ -71,7 +71,7 @@ void ButtonMeasure_Click(object sender, EventArgs e) var index = Convert.ToByte(((Button)sender).Tag); for (byte i = 0; i < 3; i++) - SpatialTransform.SetMatrixAElement(float.NaN, index, i); + SpatialTransform.A = SetMatrixElement(SpatialTransform.A, float.NaN, index, i); ts4231TextBoxes[index].Text = ""; if (((Button)sender).Text == "Measure") @@ -123,9 +123,9 @@ void ButtonMeasure_Click(object sender, EventArgs e) acc => { var measurement = acc.Sum / NumMeasurements; - SpatialTransform.SetMatrixAElement(measurement.X, index, 0); - SpatialTransform.SetMatrixAElement(measurement.Y, index, 1); - SpatialTransform.SetMatrixAElement(measurement.Z, index, 2); + SpatialTransform.A = SetMatrixElement(SpatialTransform.A, measurement.X, index, 0); + SpatialTransform.A = SetMatrixElement(SpatialTransform.A, measurement.Y, index, 1); + SpatialTransform.A = SetMatrixElement(SpatialTransform.A, measurement.Z, index, 2); return (Position: measurement, Valid: acc.Count == NumMeasurements); }) .ObserveOn(new ControlScheduler(this)) @@ -156,22 +156,22 @@ void ButtonOK_Click(object sender, EventArgs e) { var confirmationMessage = ""; var invalidInput = false; - if (SpatialTransform.ContainsNaN(SpatialTransform.A) || SpatialTransform.ContainsNaN(SpatialTransform.B)) + if (ContainsNaN(SpatialTransform.A) || ContainsNaN(SpatialTransform.B)) { - confirmationMessage = $"At least one entry in the {Name} is invalid for calculating a proper 3D spatial transform:\n"; + confirmationMessage = $"At least one entry in the TS4231V1 Calibration GUI form is invalid:\n\n"; + + for (byte i = 0; i < 4; i++) + if (float.IsNaN(MatrixToFloatArray(SpatialTransform.A)[i * 3])) + confirmationMessage += $" • TS4231 coordinate {i}\n"; var axes = new char[] { 'X', 'Y', 'Z' }; var coordinates = new byte[] { 0, 1, 2, 3 }; for (byte i = 0; i < 12; i++) - if (float.IsNaN(SpatialTransform.MatrixToFloatArray(SpatialTransform.B)[i])) + if (float.IsNaN(MatrixToFloatArray(SpatialTransform.B)[i])) confirmationMessage += $" • Component {axes[i % 3]} from user coordinate {coordinates[i / 3]}\n"; - for (byte i = 0; i < 4; i++) - if (float.IsNaN(SpatialTransform.MatrixToFloatArray(SpatialTransform.A)[i * 3])) - confirmationMessage += $" • TS4231 Coordinate {i}\n"; - - confirmationMessage += "\nThese invalid entries will not be saved. "; + confirmationMessage += "\nAny invalid entry will not be saved. "; invalidInput = true; } else if (!Matrix4x4.Invert(SpatialTransform.M, out _)) @@ -182,7 +182,7 @@ void ButtonOK_Click(object sender, EventArgs e) if (invalidInput) { - confirmationMessage += "The transformed position data will be NaNs until these entries are fixed.\n\n" + + confirmationMessage += "The transformed position data will be NaNs until all entries are valid.\n\n" + "Would you like to continue?"; if (MessageBox.Show(confirmationMessage, "Confirmation", MessageBoxButtons.YesNo) == DialogResult.Yes) DialogResult = DialogResult.OK; @@ -199,7 +199,7 @@ void EnableButtons(bool enable, byte index) void IndicateSpatialTransformStatus() { - if (SpatialTransform.ContainsNaN(SpatialTransform.A) || SpatialTransform.ContainsNaN(SpatialTransform.B)) + if (ContainsNaN(SpatialTransform.A) || ContainsNaN(SpatialTransform.B)) { toolStripStatusLabel.Image = Properties.Resources.StatusWarningImage; toolStripStatusLabel.Text = "All fields must be properly populated."; @@ -218,6 +218,37 @@ void IndicateSpatialTransformStatus() textBoxSpatialTransformMatrix.Text = SpatialTransform.M.ToString(); } } + + static float[] MatrixToFloatArray(Matrix4x4 m) => + new float[] { m.M11, m.M12, m.M13, + m.M21, m.M22, m.M23, + m.M31, m.M32, m.M33, + m.M41, m.M42, m.M43 }; + + static bool ContainsNaN(Matrix4x4 m) => MatrixToFloatArray(m).Any(float.IsNaN); + + static Matrix4x4 SetMatrixElement(Matrix4x4 m, float value, int coordinate, int component) + { + if (coordinate is < 0 or > 3) throw new ArgumentOutOfRangeException(nameof(coordinate) + " must be 0, 1, 2, or 3."); + if (component is < 0 or > 2) throw new ArgumentOutOfRangeException(nameof(component) + " must be 0, 1, or 2."); + + switch ((coordinate, component)) + { + case (0, 0): m.M11 = value; break; + case (0, 1): m.M12 = value; break; + case (0, 2): m.M13 = value; break; + case (1, 0): m.M21 = value; break; + case (1, 1): m.M22 = value; break; + case (1, 2): m.M23 = value; break; + case (2, 0): m.M31 = value; break; + case (2, 1): m.M32 = value; break; + case (2, 2): m.M33 = value; break; + case (3, 0): m.M41 = value; break; + case (3, 1): m.M42 = value; break; + case (3, 2): m.M43 = value; break; + } + return m; + } void richTextBoxInstructions_ContentsResized(object sender, ContentsResizedEventArgs e) { ((RichTextBox)sender).Height = e.NewRectangle.Height; diff --git a/OpenEphys.Onix1/OpenEphys.Onix1.csproj b/OpenEphys.Onix1/OpenEphys.Onix1.csproj index 24e6912a..cf6b5a4b 100644 --- a/OpenEphys.Onix1/OpenEphys.Onix1.csproj +++ b/OpenEphys.Onix1/OpenEphys.Onix1.csproj @@ -10,10 +10,6 @@ True - - - - diff --git a/OpenEphys.Onix1/SpatialTransform3D.cs b/OpenEphys.Onix1/SpatialTransform3D.cs index 15dd1959..e03e8795 100644 --- a/OpenEphys.Onix1/SpatialTransform3D.cs +++ b/OpenEphys.Onix1/SpatialTransform3D.cs @@ -12,21 +12,19 @@ namespace OpenEphys.Onix1 public class SpatialTransform3D { - private Matrix4x4 _a, _b; + Matrix4x4 a, b; /// /// The A matrix in A * = . It is - /// constructed from a set of four Cartesian coordinates before - /// undergoing a spatial transformation. + /// constructed from a set of four pre-transform Cartesian coordinates. /// - public Matrix4x4 A { get => _a; set { _a = value; UpdateM(); } } + public Matrix4x4 A { get => a; set { a = value; M = UpdateM(A, B); } } /// /// The B matrix in * = B. It is - /// constructed from a set of four Cartesian coordinates after - /// undergoing a spatial transformation. + /// constructed from a set of four post-transform Cartesian coordinates. /// - public Matrix4x4 B { get => _b ; set { _b = value; UpdateM(); } } + public Matrix4x4 B { get => b; set { b = value; M = UpdateM(A, B); } } /// /// The M matrix in * = M. It is the @@ -62,59 +60,20 @@ public SpatialTransform3D(SpatialTransform3D other) B = other.B; } - /// - /// Sets a component (X, Y, or Z) in one of the coordinates in - /// PreTransformCoordinates. - /// - public void SetMatrixAElement(float value, int coordinate, int component) => - SetMatrixElement(ref _a, value, coordinate, component); - - /// - /// Sets a component (X, Y, or Z) in one of the coordinates in - /// PostTransformCoordinates. - /// - public void SetMatrixBElement(float value, int coordinate, int component) => - SetMatrixElement(ref _b, value, coordinate, component); - - private void SetMatrixElement(ref Matrix4x4 m, float value, int coordinate, int component) + static Matrix4x4 UpdateM(Matrix4x4 a, Matrix4x4 b) { - if (coordinate is < 0 or > 3) throw new ArgumentOutOfRangeException(nameof(coordinate) + " must be 0, 1, 2, or 3."); - if (component is < 0 or > 2) throw new ArgumentOutOfRangeException(nameof(component) + " must be 0, 1, or 2."); - - switch ((coordinate, component)) - { - case (0, 0): m.M11 = value; break; case (0, 1): m.M12 = value; break; case (0, 2): m.M13 = value; break; - case (1, 0): m.M21 = value; break; case (1, 1): m.M22 = value; break; case (1, 2): m.M23 = value; break; - case (2, 0): m.M31 = value; break; case (2, 1): m.M32 = value; break; case (2, 2): m.M33 = value; break; - case (3, 0): m.M41 = value; break; case (3, 1): m.M42 = value; break; case (3, 2): m.M43 = value; break; - } - UpdateM(); + Matrix4x4.Invert(a, out var aInverted); + var m = Matrix4x4.Multiply(aInverted, b); + if (Matrix4x4.Invert(m, out _) && !new float[] { m.M11, m.M12, m.M13, m.M14, + m.M21, m.M22, m.M23, m.M24, + m.M31, m.M32, m.M33, m.M34, + m.M41, m.M42, m.M43, m.M44 }.Any(float.IsNaN)) + return m; + else + return new(float.NaN, float.NaN, float.NaN, float.NaN, + float.NaN, float.NaN, float.NaN, float.NaN, + float.NaN, float.NaN, float.NaN, float.NaN, + float.NaN, float.NaN, float.NaN, float.NaN); } - - private void UpdateM() - { - - Matrix4x4.Invert(A, out var AInverted); - var m = Matrix4x4.Multiply(AInverted, B); - M = !ContainsNaN(m) && Matrix4x4.Invert(m, out _) ? m : - new(float.NaN, float.NaN, float.NaN, float.NaN, - float.NaN, float.NaN, float.NaN, float.NaN, - float.NaN, float.NaN, float.NaN, float.NaN, - float.NaN, float.NaN, float.NaN, float.NaN); - } - - /// - /// Convert coordinates from matrix to a float array. - /// - public float[] MatrixToFloatArray(Matrix4x4 m) => - new float[] { m.M11, m.M12, m.M13, - m.M21, m.M22, m.M23, - m.M31, m.M32, m.M33, - m.M41, m.M42, m.M43 }; - - /// - /// Checks if matrix contains one or more NaNs. - /// - public bool ContainsNaN(Matrix4x4 m) => MatrixToFloatArray(m).Any(float.IsNaN); } } diff --git a/OpenEphys.Onix1/TS4231V1TransformedPositionData.bonsai b/OpenEphys.Onix1/TS4231V1TransformedPositionData.bonsai deleted file mode 100644 index 60dd9e21..00000000 --- a/OpenEphys.Onix1/TS4231V1TransformedPositionData.bonsai +++ /dev/null @@ -1,89 +0,0 @@ - - - - - - - - - - - 0 - 0 - 0 - - - 1 - 0 - 0 - - - - - - - - - - - NaN - NaN - NaN - 1 - NaN - NaN - NaN - 1 - NaN - NaN - NaN - 1 - NaN - NaN - NaN - 1 - - NaN - NaN - NaN - - - - NaN - NaN - NaN - 1 - NaN - NaN - NaN - 1 - NaN - NaN - NaN - 1 - NaN - NaN - NaN - 1 - - NaN - NaN - NaN - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/spatial-transform_issue-427.patch b/spatial-transform_issue-427.patch deleted file mode 100644 index b31e8ac2..00000000 --- a/spatial-transform_issue-427.patch +++ /dev/null @@ -1,599 +0,0 @@ -diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs -index c5530bf..b0dcef2 100644 ---- a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs -+++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs -@@ -1,11 +1,10 @@ - using System; -+using System.Drawing; - using System.Linq; - using System.Numerics; --using System.Windows.Forms; - using System.Reactive.Linq; -+using System.Windows.Forms; - using Bonsai.Design; --using System.Collections.Generic; --using System.Drawing; - - namespace OpenEphys.Onix1.Design - { -@@ -14,58 +13,61 @@ namespace OpenEphys.Onix1.Design - /// - public partial class SpatialTransformMatrixDialog : Form - { -- internal SpatialTransformProperties SpatialTransform; -+ internal SpatialTransform3D SpatialTransform; - const byte NumMeasurements = 100; - readonly IObservable PositionDataSource; - IDisposable richTextBoxStatusUpdateSubscription; - IDisposable MeasurementCalculationSubscription; - -- internal SpatialTransformMatrixDialog(IObservable dataSource, SpatialTransformProperties transformProperties) -+ internal SpatialTransformMatrixDialog(IObservable dataSource, SpatialTransform3D transformProperties) - { - InitializeComponent(); - SpatialTransform = transformProperties; - PositionDataSource = dataSource; - -- var ts4231TextBoxes = new TextBox[] { -- textBoxTS4231Coordinate0, textBoxTS4231Coordinate1, -- textBoxTS4231Coordinate2, textBoxTS4231Coordinate3}; -- foreach (var (textBox, v) in Enumerable.Zip(ts4231TextBoxes, SpatialTransform.Pre, (tb, v) => (tb, v))) -- textBox.Text = checkVector3ForNaN(v) ? "" : $"{v.X}, {v.Y}, {v.Z}"; -+ var ts4231TextBoxes = new TextBox[] { -+ textBoxTS4231Coordinate0, textBoxTS4231Coordinate1, -+ textBoxTS4231Coordinate2, textBoxTS4231Coordinate3 }; -+ var preTransformCoordinates = SpatialTransform.MatrixToFloatArray(SpatialTransform.A); -+ for (byte i = 0; i < 3; i++) -+ ts4231TextBoxes[i].Text = float.IsNaN(preTransformCoordinates[i * 3]) ? "" : $"{preTransformCoordinates[i * 3]}, " + -+ $"{preTransformCoordinates[i * 3 + 1]}, " + -+ $"{preTransformCoordinates[i * 3 + 2]}"; - - var userTextBoxes = new TextBox[] { - textBoxUserCoordinate0X, textBoxUserCoordinate0Y, textBoxUserCoordinate0Z, - textBoxUserCoordinate1X, textBoxUserCoordinate1Y, textBoxUserCoordinate1Z, - textBoxUserCoordinate2X, textBoxUserCoordinate2Y, textBoxUserCoordinate2Z, -- textBoxUserCoordinate3X, textBoxUserCoordinate3Y, textBoxUserCoordinate3Z}; -- for (byte i = 0; i < 12; i++) -- { -- ref var component = ref GetComponent(ref SpatialTransform.Post[i / 3], i % 3); -- userTextBoxes[i].Text = float.IsNaN(component) ? "" : component.ToString(); -- } -+ textBoxUserCoordinate3X, textBoxUserCoordinate3Y, textBoxUserCoordinate3Z }; -+ var postTransformCoordinates = SpatialTransform.MatrixToFloatArray(SpatialTransform.B); -+ foreach (var (tb, comp) in Enumerable.Zip(userTextBoxes, postTransformCoordinates, (tb, comp) => (tb, comp))) -+ tb.Text = float.IsNaN(comp) ? "" : comp.ToString(); - -- CalculatePrintMatrix(); -+ IndicateSpatialTransformStatus(); - } - - private void TextBoxUserCoordinate_TextChanged(object sender, EventArgs e) - { - var tag = Convert.ToByte(((TextBox)sender).Tag); -- ref var coordinateComponent = ref GetComponent(ref SpatialTransform.Post[tag / 3], tag % 3); -- try { coordinateComponent = float.Parse(((TextBox)sender).Text); } -- catch { coordinateComponent = float.NaN; } -- CalculatePrintMatrix(); -+ try { SpatialTransform.SetMatrixBElement(float.Parse(((TextBox)sender).Text), tag / 3, tag % 3); } -+ catch { SpatialTransform.SetMatrixBElement(float.NaN, tag / 3, tag % 3); } -+ IndicateSpatialTransformStatus(); - } - - private void ButtonMeasure_Click(object sender, EventArgs e) - { - TextBox[] ts4231TextBoxes = { textBoxTS4231Coordinate0, textBoxTS4231Coordinate1, textBoxTS4231Coordinate2, textBoxTS4231Coordinate3 }; - var index = Convert.ToByte(((Button)sender).Tag); -+ -+ for (byte i = 0; i < 3; i++) -+ SpatialTransform.SetMatrixAElement(float.NaN, index, i); - ts4231TextBoxes[index].Text = ""; -- SpatialTransform.Pre[index] = new(float.NaN); -+ - if (((Button)sender).Text == "Measure") - { - richTextBoxStatus.SelectionColor = Color.Blue; - richTextBoxStatus.AppendText($"Measurement at coordinate {index} initiated.\n"); -- SpatialTransform.M = null; -+ IndicateSpatialTransformStatus(); - textBoxSpatialTransformMatrix.Text = ""; - ((Button)sender).Text = "Cancel"; - EnableButtons(false, index); -@@ -109,8 +111,12 @@ namespace OpenEphys.Onix1.Design - (acc, current) => (acc.Sum + current.Position, acc.Count + 1), - acc => - { -- SpatialTransform.Pre[index] = acc.Sum / NumMeasurements; -- return (Position: SpatialTransform.Pre[index], Valid: acc.Count == NumMeasurements); -+ var measurement = acc.Sum / NumMeasurements; -+ SpatialTransform.SetMatrixAElement(measurement.X, index, 0); -+ SpatialTransform.SetMatrixAElement(measurement.Y, index, 1); -+ SpatialTransform.SetMatrixAElement(measurement.Z, index, 2); -+ Console.WriteLine(SpatialTransform.A.ToString()); -+ return (Position: measurement, Valid: acc.Count == NumMeasurements); - }) - .ObserveOn(new ControlScheduler(this)) - .Subscribe(measurement => -@@ -119,7 +125,7 @@ namespace OpenEphys.Onix1.Design - if (measurement.Valid) - { - ts4231TextBoxes[index].Text = $"{measurement.Position.X}, {measurement.Position.Y}, {measurement.Position.Z}"; -- CalculatePrintMatrix(); -+ IndicateSpatialTransformStatus(); - } - }); - -@@ -138,106 +144,69 @@ namespace OpenEphys.Onix1.Design - - private void ButtonOK_Click(object sender, EventArgs e) - { -- if (SpatialTransform.M.HasValue) -- DialogResult = DialogResult.OK; -- else -+ var confirmationMessage = ""; -+ var invalidInput = false; -+ if (SpatialTransform.ContainsNaN(SpatialTransform.A) || SpatialTransform.ContainsNaN(SpatialTransform.B)) - { -- var confirmationMessage = ""; -- var incompleteInput = false; -- if (SpatialTransform.Post.Any(userCoordinate => checkVector3ForNaN(userCoordinate))) -- { -- incompleteInput = true; -- var axes = new char[] { 'X', 'Y', 'Z' }; -- var coordinates = new byte[] { 0, 1, 2, 3 }; -- confirmationMessage += "At least one coordinate component is empty or invalid:\n"; -- for (byte i = 0; i < 12; i++) -- { -- ref var component = ref GetComponent(ref SpatialTransform.Post[i / 3], i % 3); -- if (float.IsNaN(component)) -- confirmationMessage += $" • Coordinate {coordinates[i / 3]} {axes[i % 3]} component\n"; -- } -- confirmationMessage += "\n"; -- } -- if (SpatialTransform.Pre.Any(TS4231Coordinate => checkVector3ForNaN(TS4231Coordinate))) -- { -- incompleteInput = true; -- confirmationMessage += "At least one coordinate measurement is empty:\n"; -- foreach (var (i, v) in SpatialTransform.Pre.Select((i, v) => (v, i))) -- if (checkVector3ForNaN(v)) -- confirmationMessage += $" • Coordinate {i}\n"; -- confirmationMessage += "\n"; -- } -- -- if (incompleteInput) -- confirmationMessage += "They will not be saved and transformed position data won't be properly output.\n\n"; -- else if (!Matrix4x4.Invert(Vector3sToMatrix4x4(SpatialTransform.Post), out _)) -- confirmationMessage = "The spatial transform matrix is non-invertible. The transformed position data won't be properly output.\n\n"; -- -- confirmationMessage += "Would you like to continue?"; -+ confirmationMessage = $"At least one entry in the {Name} is invalid for calculating a proper 3D spatial transform:\n"; -+ -+ var axes = new char[] { 'X', 'Y', 'Z' }; -+ var coordinates = new byte[] { 0, 1, 2, 3 }; -+ -+ for (byte i = 0; i < 12; i++) -+ if (float.IsNaN(SpatialTransform.MatrixToFloatArray(SpatialTransform.B)[i])) -+ confirmationMessage += $" • Component {axes[i % 3]} from user coordinate {coordinates[i / 3]}\n"; - -+ for (byte i = 0; i < 4; i++) -+ if (float.IsNaN(SpatialTransform.MatrixToFloatArray(SpatialTransform.A)[i * 3])) -+ confirmationMessage += $" • TS4231 Coordinate {i}\n"; -+ -+ confirmationMessage += "\nThese invalid entries will not be saved. "; -+ invalidInput = true; -+ } -+ else if (!Matrix4x4.Invert(SpatialTransform.M, out _)) -+ { -+ confirmationMessage = $"The calculated spatial transform matrix is non-invertible\n"; -+ invalidInput = true; -+ } -+ -+ if (invalidInput) -+ { -+ confirmationMessage += "The transformed position data will be zeros until these entries are fixed.\n\n" + -+ "Would you like to continue?"; - if (MessageBox.Show(confirmationMessage, "Confirmation", MessageBoxButtons.YesNo) == DialogResult.Yes) - DialogResult = DialogResult.OK; -- } -+ } -+ else -+ DialogResult = DialogResult.OK; - } - -- private readonly Func checkVector3ForNaN = v => new[] { v.X, v.Y, v.Z }.Any(float.IsNaN); -- - private void EnableButtons(bool enable, byte index) - { - var buttons = new Button[] { buttonMeasure0, buttonMeasure1, buttonMeasure2, buttonMeasure3, buttonOK, buttonCancel }; - Array.ForEach(buttons, button => button.Enabled = enable || (Convert.ToByte(button.Tag) == index)); - } - -- private void CalculatePrintMatrix() -+ private void IndicateSpatialTransformStatus() - { -- SpatialTransform.M = null; -- if (!SpatialTransform.Post.Any(userCoordinate => checkVector3ForNaN(userCoordinate)) && -- !SpatialTransform.Pre.Any(TS4231Coordinate => checkVector3ForNaN(TS4231Coordinate))) -- { -- if (Matrix4x4.Invert(Vector3sToMatrix4x4(SpatialTransform.Post), out _)) -- { -- var ts4231V1CoordinatesMatrix = Vector3sToMatrix4x4(SpatialTransform.Pre); -- var userCoordinatesMatrix = Vector3sToMatrix4x4(SpatialTransform.Post); -- Matrix4x4.Invert(ts4231V1CoordinatesMatrix, out var ts4231V1CoordinatesMatrixInverted); -- SpatialTransform.M = Matrix4x4.Multiply(ts4231V1CoordinatesMatrixInverted, userCoordinatesMatrix); -- toolStripStatusLabel.Image = Properties.Resources.StatusReadyImage; -- toolStripStatusLabel.Text = "Spatial transform matrix is calculated."; -- } -- else -- { -- toolStripStatusLabel.Image = Properties.Resources.StatusWarningImage; -- toolStripStatusLabel.Text = "The resulting spatial transform matrix must be non-invertible."; -- } -- } -- else -+ if (SpatialTransform.ContainsNaN(SpatialTransform.A) || SpatialTransform.ContainsNaN(SpatialTransform.B)) - { - toolStripStatusLabel.Image = Properties.Resources.StatusWarningImage; - toolStripStatusLabel.Text = "All fields must be properly populated."; -+ textBoxSpatialTransformMatrix.Text = ""; - } -- if (SpatialTransform.M.HasValue) -- textBoxSpatialTransformMatrix.Text = SpatialTransform.M.Value.ToString(); -- else -+ else if (!Matrix4x4.Invert(SpatialTransform.M, out _)) -+ { -+ toolStripStatusLabel.Image = Properties.Resources.StatusWarningImage; -+ toolStripStatusLabel.Text = "The calculated spatial transform matrix must be invertible."; - textBoxSpatialTransformMatrix.Text = ""; -- } -- -- private static ref float GetComponent(ref Vector3 v, int index) -- { -- switch (index) -+ } -+ else - { -- case 0: return ref v.X; -- case 1: return ref v.Y; -- case 2: return ref v.Z; -- default: throw new IndexOutOfRangeException(); -- }; -- } -- -- private Matrix4x4 Vector3sToMatrix4x4(IList rows) -- { -- return new Matrix4x4( -- rows[0].X, rows[0].Y, rows[0].Z, 1, -- rows[1].X, rows[1].Y, rows[1].Z, 1, -- rows[2].X, rows[2].Y, rows[2].Z, 1, -- rows[3].X, rows[3].Y, rows[3].Z, 1); -+ toolStripStatusLabel.Image = Properties.Resources.StatusReadyImage; -+ toolStripStatusLabel.Text = "Spatial transform matrix is calculated."; -+ textBoxSpatialTransformMatrix.Text = SpatialTransform.M.ToString(); -+ } - } - } - } -diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs b/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs -index 66f8bca..6be7ac3 100644 ---- a/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs -+++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixEditor.cs -@@ -33,7 +33,7 @@ namespace OpenEphys.Onix1.Design - { - var source = GetDataSource(context, provider); - var dataFrames = source.Output.Merge().Select(x => x as TS4231V1PositionDataFrame); -- using var visualizerDialog = new SpatialTransformMatrixDialog(dataFrames, new SpatialTransformProperties((SpatialTransformProperties)value)); -+ using var visualizerDialog = new SpatialTransformMatrixDialog(dataFrames, new SpatialTransform3D((SpatialTransform3D)value)); - if (!editorState.WorkflowRunning) - { - throw new InvalidOperationException("Workflow must be running to open this GUI."); -diff --git a/OpenEphys.Onix1/SpatialTransform3D.cs b/OpenEphys.Onix1/SpatialTransform3D.cs -new file mode 100644 -index 0000000..4b9c2b8 ---- /dev/null -+++ b/OpenEphys.Onix1/SpatialTransform3D.cs -@@ -0,0 +1,122 @@ -+using System; -+using System.Linq; -+using System.Numerics; -+using System.Xml.Serialization; -+ -+namespace OpenEphys.Onix1 -+{ -+ /// -+ /// Data necessary to construct a spatial transform matrix as well as the -+ /// spatial transform matrix itself. -+ /// -+ public class SpatialTransform3D -+ { -+ -+ private Matrix4x4 _a, _b; -+ -+ /// -+ /// The A matrix in A * = . It is -+ /// constructed from a set of four Cartesian coordinates before -+ /// undergoing a spatial transformation. -+ /// -+ public Matrix4x4 A { get => _a; set { _a = value; UpdateM(); } } -+ -+ /// -+ /// The B matrix in * = B. It is -+ /// constructed from a set of four Cartesian coordinates after -+ /// undergoing a spatial transformation. -+ /// -+ public Matrix4x4 B { get => _b ; set { _b = value; UpdateM(); } } -+ -+ /// -+ /// The M matrix in * = M. It is the -+ /// spatial transform matrix. It calculated as M = A.inv * B. -+ /// -+ [XmlIgnore] -+ public Matrix4x4 M { get; private set; } -+ -+ /// -+ /// Initializes a new instance of the -+ /// class with default values. -+ /// -+ public SpatialTransform3D() -+ { -+ A = B = new(float.NaN, float.NaN, float.NaN, 1, -+ float.NaN, float.NaN, float.NaN, 1, -+ float.NaN, float.NaN, float.NaN, 1, -+ float.NaN, float.NaN, float.NaN, 1); -+ M = new(float.NaN, float.NaN, float.NaN, float.NaN, -+ float.NaN, float.NaN, float.NaN, float.NaN, -+ float.NaN, float.NaN, float.NaN, float.NaN, -+ float.NaN, float.NaN, float.NaN, float.NaN); -+ } -+ -+ /// -+ /// Initializes a new instance of the -+ /// class as a copy of an existing instance. -+ /// -+ /// The instance to copy. -+ public SpatialTransform3D(SpatialTransform3D other) -+ { -+ A = other.A; -+ B = other.B; -+ } -+ -+ /// -+ /// Sets a component (X, Y, or Z) in one of the coordinates in -+ /// PreTransformCoordinates. -+ /// -+ public void SetMatrixAElement(float value, int coordinate, int component) => -+ SetMatrixElement(ref _a, value, coordinate, component); -+ -+ /// -+ /// Sets a component (X, Y, or Z) in one of the coordinates in -+ /// PostTransformCoordinates. -+ /// -+ public void SetMatrixBElement(float value, int coordinate, int component) => -+ SetMatrixElement(ref _b, value, coordinate, component); -+ -+ private void SetMatrixElement(ref Matrix4x4 m, float value, int coordinate, int component) -+ { -+ if (coordinate is < 0 or > 3) throw new ArgumentOutOfRangeException(nameof(coordinate) + " must be 0, 1, 2, or 3."); -+ if (component is < 0 or > 2) throw new ArgumentOutOfRangeException(nameof(component) + " must be 0, 1, or 2."); -+ -+ switch ((coordinate, component)) -+ { -+ case (0, 0): m.M11 = value; break; -+ case (0, 1): m.M12 = value; break; -+ case (0, 2): m.M13 = value; break; -+ case (1, 0): m.M21 = value; break; -+ case (1, 1): m.M22 = value; break; -+ case (1, 2): m.M23 = value; break; -+ case (2, 0): m.M31 = value; break; -+ case (2, 1): m.M32 = value; break; -+ case (2, 2): m.M33 = value; break; -+ case (3, 0): m.M41 = value; break; -+ case (3, 1): m.M42 = value; break; -+ case (3, 2): m.M43 = value; break; -+ } -+ UpdateM(); -+ } -+ -+ private void UpdateM() -+ { -+ Matrix4x4.Invert(A, out var AInverted); -+ M = Matrix4x4.Multiply(AInverted, B); -+ } -+ -+ /// -+ /// Convert coordinates from matrix to a float array. -+ /// -+ public float[] MatrixToFloatArray(Matrix4x4 m) => -+ new float[] { m.M11, m.M12, m.M13, -+ m.M21, m.M22, m.M23, -+ m.M31, m.M32, m.M33, -+ m.M41, m.M42, m.M43 }; -+ -+ /// -+ /// Checks if matrix contains one or more NaNs. -+ /// -+ public bool ContainsNaN(Matrix4x4 m) => MatrixToFloatArray(m).Any(float.IsNaN); -+ } -+} -diff --git a/OpenEphys.Onix1/SpatialTransformProperties.cs b/OpenEphys.Onix1/SpatialTransformProperties.cs -deleted file mode 100644 -index f20306b..0000000 ---- a/OpenEphys.Onix1/SpatialTransformProperties.cs -+++ /dev/null -@@ -1,58 +0,0 @@ --using System; --using System.Collections.Generic; --using System.Linq; --using System.Numerics; --using System.Text; --using System.Threading.Tasks; -- --namespace OpenEphys.Onix1 --{ -- /// -- /// Data necessary to construct a spatial transform matrix as well as the -- /// spatial transform matrix itself. -- /// -- public class SpatialTransformProperties -- { -- /// -- /// The set of coordinates before undergoing a spatial transform. -- /// -- public Vector3[] Pre { get; set; } -- -- /// -- /// The set of coordinates after undergoing a spatial transform. -- /// -- public Vector3[] Post { get; set; } -- -- /// -- /// The spatial transform matrix calculated from and -- /// . -- /// -- public Matrix4x4? M { get; set; } -- -- /// -- /// Initializes a new instance of the class with default values. -- /// -- public SpatialTransformProperties() -- { -- Pre = new Vector3[] { new(float.NaN), new(float.NaN), new(float.NaN), new(float.NaN) }; -- Post = new Vector3[] { new(float.NaN), new(float.NaN), new(float.NaN), new(float.NaN) }; -- M = null; -- } -- -- /// -- /// Initializes a new instance of the class as a copy of an existing -- /// instance. -- /// -- /// The instance to copy. -- public SpatialTransformProperties(SpatialTransformProperties other) -- { -- Pre = new Vector3[4]; -- Post = new Vector3[4]; -- Array.Copy(other.Pre, Pre, 4); -- Array.Copy(other.Post, Post, 4); -- M = other.M; -- } -- } --} -diff --git a/OpenEphys.Onix1/TS4231V1SpatialTransform.cs b/OpenEphys.Onix1/TS4231V1SpatialTransform.cs -index 7176f6d..7475130 100644 ---- a/OpenEphys.Onix1/TS4231V1SpatialTransform.cs -+++ b/OpenEphys.Onix1/TS4231V1SpatialTransform.cs -@@ -19,7 +19,7 @@ namespace OpenEphys.Onix1 - /// - [Editor("OpenEphys.Onix1.Design.SpatialTransformMatrixEditor, OpenEphys.Onix1.Design", DesignTypes.UITypeEditor)] - [Description("Data for transforming position measurements to another reference frame.")] -- public SpatialTransformProperties SpatialTransform { get; set; } = new(); -+ public SpatialTransform3D SpatialTransform { get; set; } = new(); - - /// - /// Transforms a sequence of -@@ -34,7 +34,7 @@ namespace OpenEphys.Onix1 - { - return source.Select(input => - new TS4231V1PositionDataFrame(input.Clock, input.HubClock, input.SensorIndex, -- Vector3.Transform(input.Position, SpatialTransform.M.GetValueOrDefault()))); -+ Vector3.Transform(input.Position, SpatialTransform.M))); - } - } - } -diff --git a/OpenEphys.Onix1/TS4231V1TransformedPositionData.bonsai b/OpenEphys.Onix1/TS4231V1TransformedPositionData.bonsai -index 9a8a7a3..60dd9e2 100644 ---- a/OpenEphys.Onix1/TS4231V1TransformedPositionData.bonsai -+++ b/OpenEphys.Onix1/TS4231V1TransformedPositionData.bonsai -@@ -1,5 +1,5 @@ -  -- -@@ -20,29 +20,6 @@ - 0 - 0 - -- -- 1 -- 0 -- 0 -- 0 -- 0 -- 1 -- 0 -- 0 -- 0 -- 0 -- 1 -- 0 -- 0 -- 0 -- 0 -- 1 -- -- 0 -- 0 -- 0 -- -- - - - -@@ -50,7 +27,54 @@ - - - -- -+ -+ -+ NaN -+ NaN -+ NaN -+ 1 -+ NaN -+ NaN -+ NaN -+ 1 -+ NaN -+ NaN -+ NaN -+ 1 -+ NaN -+ NaN -+ NaN -+ 1 -+ -+ NaN -+ NaN -+ NaN -+ -+ -+ -+ NaN -+ NaN -+ NaN -+ 1 -+ NaN -+ NaN -+ NaN -+ 1 -+ NaN -+ NaN -+ NaN -+ 1 -+ NaN -+ NaN -+ NaN -+ 1 -+ -+ NaN -+ NaN -+ NaN -+ -+ -+ - - - From bf2e3af32c1c282bad49bf9e2c5bd0e686375db7 Mon Sep 17 00:00:00 2001 From: cjsha Date: Sat, 30 Aug 2025 22:14:31 -0400 Subject: [PATCH 17/17] Prettify Matrix string Also fix instructions at top --- .../SpatialTransformMatrixDialog.Designer.cs | 23 ++++---- .../SpatialTransformMatrixDialog.cs | 53 ++++++++++++++++++- 2 files changed, 64 insertions(+), 12 deletions(-) diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.Designer.cs b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.Designer.cs index 2663b204..80fade8f 100644 --- a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.Designer.cs +++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.Designer.cs @@ -100,11 +100,12 @@ private void InitializeComponent() this.tableLayoutPanelMain.RowStyles.Add(new System.Windows.Forms.RowStyle()); this.tableLayoutPanelMain.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F)); this.tableLayoutPanelMain.RowStyles.Add(new System.Windows.Forms.RowStyle()); - this.tableLayoutPanelMain.Size = new System.Drawing.Size(624, 540); + this.tableLayoutPanelMain.Size = new System.Drawing.Size(624, 640); this.tableLayoutPanelMain.TabIndex = 0; // // groupBoxStatus // + this.groupBoxStatus.AutoSize = true; this.groupBoxStatus.Controls.Add(this.richTextBoxStatus); this.groupBoxStatus.Dock = System.Windows.Forms.DockStyle.Fill; this.groupBoxStatus.Location = new System.Drawing.Point(3, 406); @@ -128,11 +129,12 @@ private void InitializeComponent() // // tableLayoutPanelCoordinates // + this.tableLayoutPanelCoordinates.AutoSize = true; this.tableLayoutPanelCoordinates.ColumnCount = 6; this.tableLayoutPanelCoordinates.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); this.tableLayoutPanelCoordinates.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle()); this.tableLayoutPanelCoordinates.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 180F)); - this.tableLayoutPanelCoordinates.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 33.33333F)); + this.tableLayoutPanelCoordinates.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 33.33332F)); this.tableLayoutPanelCoordinates.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 33.33334F)); this.tableLayoutPanelCoordinates.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 33.33334F)); this.tableLayoutPanelCoordinates.Controls.Add(this.labelZ, 6, 1); @@ -540,8 +542,8 @@ private void InitializeComponent() this.tableLayoutPanelSpatialMatrix.Location = new System.Drawing.Point(3, 340); this.tableLayoutPanelSpatialMatrix.Name = "tableLayoutPanelSpatialMatrix"; this.tableLayoutPanelSpatialMatrix.RowCount = 1; - this.tableLayoutPanelSpatialMatrix.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 60F)); - this.tableLayoutPanelSpatialMatrix.Size = new System.Drawing.Size(618, 60); + this.tableLayoutPanelSpatialMatrix.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 70F)); + this.tableLayoutPanelSpatialMatrix.Size = new System.Drawing.Size(618, 80); this.tableLayoutPanelSpatialMatrix.TabIndex = 0; // // labelSpatialMatrix @@ -550,7 +552,7 @@ private void InitializeComponent() this.labelSpatialMatrix.Dock = System.Windows.Forms.DockStyle.Fill; this.labelSpatialMatrix.Location = new System.Drawing.Point(3, 0); this.labelSpatialMatrix.Name = "labelSpatialMatrix"; - this.labelSpatialMatrix.Size = new System.Drawing.Size(123, 60); + this.labelSpatialMatrix.Size = new System.Drawing.Size(123, 80); this.labelSpatialMatrix.TabIndex = 1000; this.labelSpatialMatrix.Text = "Spatial Transform Matrix:"; // @@ -559,11 +561,12 @@ private void InitializeComponent() this.textBoxSpatialTransformMatrix.AcceptsReturn = true; this.textBoxSpatialTransformMatrix.AcceptsTab = true; this.textBoxSpatialTransformMatrix.Dock = System.Windows.Forms.DockStyle.Fill; + this.textBoxSpatialTransformMatrix.Font = new System.Drawing.Font("Courier New", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); this.textBoxSpatialTransformMatrix.Location = new System.Drawing.Point(132, 3); this.textBoxSpatialTransformMatrix.Multiline = true; this.textBoxSpatialTransformMatrix.Name = "textBoxSpatialTransformMatrix"; this.textBoxSpatialTransformMatrix.ReadOnly = true; - this.textBoxSpatialTransformMatrix.Size = new System.Drawing.Size(483, 54); + this.textBoxSpatialTransformMatrix.Size = new System.Drawing.Size(483, 74); this.textBoxSpatialTransformMatrix.TabIndex = 1000; this.textBoxSpatialTransformMatrix.TabStop = false; // @@ -576,7 +579,7 @@ private void InitializeComponent() this.richTextBoxInstructions.Name = "richTextBoxInstructions"; this.richTextBoxInstructions.ReadOnly = true; this.richTextBoxInstructions.ScrollBars = System.Windows.Forms.RichTextBoxScrollBars.None; - this.richTextBoxInstructions.Size = new System.Drawing.Size(618, 145); + this.richTextBoxInstructions.Size = new System.Drawing.Size(615, 145); this.richTextBoxInstructions.TabIndex = 1000; this.richTextBoxInstructions.TabStop = false; this.richTextBoxInstructions.Text = "Placeholder text"; @@ -587,7 +590,7 @@ private void InitializeComponent() this.statusStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { this.toolStripStatusLabel}); this.statusStrip.LayoutStyle = System.Windows.Forms.ToolStripLayoutStyle.Flow; - this.statusStrip.Location = new System.Drawing.Point(0, 540); + this.statusStrip.Location = new System.Drawing.Point(0, 640); this.statusStrip.Name = "statusStrip"; this.statusStrip.ShowItemToolTips = true; this.statusStrip.Size = new System.Drawing.Size(624, 21); @@ -604,11 +607,11 @@ private void InitializeComponent() // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(624, 561); + this.ClientSize = new System.Drawing.Size(624, 661); this.Controls.Add(this.tableLayoutPanelMain); this.Controls.Add(this.statusStrip); this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); - this.MinimumSize = new System.Drawing.Size(640, 600); + this.MinimumSize = new System.Drawing.Size(640, 700); this.Name = "SpatialTransformMatrixDialog"; this.Text = "TS4231V1 Calibration GUI"; this.tableLayoutPanelMain.ResumeLayout(false); diff --git a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs index c936bdcd..37018ad2 100644 --- a/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs +++ b/OpenEphys.Onix1.Design/SpatialTransformMatrixDialog.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Numerics; using System.Reactive.Linq; +using System.Text; using System.Windows.Forms; using Bonsai.Design; @@ -25,7 +26,7 @@ internal SpatialTransformMatrixDialog(IObservable dat richTextBoxInstructions.Clear(); richTextBoxInstructions.BulletIndent = 16; - richTextBoxInstructions.SelectedText = "The following is a list of bulleted items:\n\n"; + richTextBoxInstructions.SelectedText = "Follow the instructions below to transfom TS4231 position data from a generic base-station reference frame to a user-define reference frame:\n\n"; richTextBoxInstructions.SelectionBullet = true; richTextBoxInstructions.SelectedText = "Determine a set of 4, well separated XYZ positions in the space in which the headstage will move. These positions should explore a large region of the territory that the headstage will explore and not be confined to a particular plane. Each position defined in this step corresponds to a row in the table below.\n"; richTextBoxInstructions.SelectedText = "For the first position, place the headstage and click the first measure button on the GUI. After the TS4231 coordinate is obtained from the headstage, enter the known User coordinates in the X, Y, and Z text boxes to provide your spatial mapping. Repeat this process for the second, third, and fourth positions to populate the second, third, and fourth rows of the table.\n"; @@ -215,7 +216,7 @@ void IndicateSpatialTransformStatus() { toolStripStatusLabel.Image = Properties.Resources.StatusReadyImage; toolStripStatusLabel.Text = "Spatial transform matrix is calculated."; - textBoxSpatialTransformMatrix.Text = SpatialTransform.M.ToString(); + textBoxSpatialTransformMatrix.Text = Matrix4x4ToPrettyString(SpatialTransform.M); } } @@ -253,5 +254,53 @@ void richTextBoxInstructions_ContentsResized(object sender, ContentsResizedEvent { ((RichTextBox)sender).Height = e.NewRectangle.Height; } + + static string Matrix4x4ToPrettyString(Matrix4x4 matrix, int decimals = 5, int padding = 15) + { + string format = $"F{decimals}"; + + string[,] elements = new string[4, 4] + { + { matrix.M11.ToString(format).PadLeft(padding), + matrix.M12.ToString(format).PadLeft(padding), + matrix.M13.ToString(format).PadLeft(padding), + matrix.M14.ToString(format).PadLeft(padding) }, + { matrix.M21.ToString(format).PadLeft(padding), + matrix.M22.ToString(format).PadLeft(padding), + matrix.M23.ToString(format).PadLeft(padding), + matrix.M24.ToString(format).PadLeft(padding) }, + { matrix.M31.ToString(format).PadLeft(padding), + matrix.M32.ToString(format).PadLeft(padding), + matrix.M33.ToString(format).PadLeft(padding), + matrix.M34.ToString(format).PadLeft(padding) }, + { matrix.M41.ToString(format).PadLeft(padding), + matrix.M42.ToString(format).PadLeft(padding), + matrix.M43.ToString(format).PadLeft(padding), + matrix.M44.ToString(format).PadLeft(padding) } + }; + + var sb = new StringBuilder(); + sb.Append("[["); + + for (int row = 0; row < 4; row++) + { + for (int col = 0; col < 4; col++) + { + sb.Append(elements[row, col]); + if (col < 3) sb.Append(","); + } + sb.Append("]"); + + if (row < 3) + { + sb.Append(","); + sb.AppendLine(); + sb.Append(" ["); + } + else + sb.Append("]"); + } + return sb.ToString(); + } } }