diff --git a/examples/Examples.vb b/examples/Examples.vb index a5b2b503..ef4b7c9c 100644 --- a/examples/Examples.vb +++ b/examples/Examples.vb @@ -1,4 +1,4 @@ -Public Class Examples +Public Class Examples ''' ''' Path to the time series file @@ -149,6 +149,12 @@ Console.WriteLine("End date: " & ts.EndDate) Console.WriteLine("Average: " & ts.Average) + 'set some custom display options + ts.DisplayOptions.Color = Color.Red + ts.DisplayOptions.LineStyle = Drawing2D.DashStyle.Dash + ts.DisplayOptions.LineWidth = 1 + ts.DisplayOptions.ShowPoints = True + 'import the series in Wave Wave1.Import_Series(ts) diff --git a/source/CHANGELOG.md b/source/CHANGELOG.md index a87893e9..beb25d23 100644 --- a/source/CHANGELOG.md +++ b/source/CHANGELOG.md @@ -6,6 +6,11 @@ Development NEW: * Added a new series selection dialog for known (non-CSV) file types * Added the ability to reorder series from within the Time Series Properties window +* Wave project file (WVP): + * Added support for additional series options in WVP files #117 + * When saving to WVP, a new dialog allows specifying which series options to save + * When saving to WVP, file paths can now optionally be saved as relative + * WVP files are now always read and written using UTF-8 encoding CHANGED: * Upgrade TeeChart to v4.2023.4.18 @@ -46,6 +51,7 @@ CHANGED: API-CHANGES: * Removed property `TimeSeriesFile.nLinesPerTimestamp` * Removed the properties `TimeSeries.Objekt` and `TimeSeries.Type` +* New public method `Fileformats.WVP.Write_File()` for writing a Wave project file Version 2.4.5 ------------- diff --git a/source/Classes/TimeSeries.vb b/source/Classes/TimeSeries.vb index aaf2b5f4..c2bbc814 100644 --- a/source/Classes/TimeSeries.vb +++ b/source/Classes/TimeSeries.vb @@ -54,6 +54,7 @@ Public Class TimeSeries Private _Type As String Private _Interpretation As InterpretationEnum Private _DataSource As TimeSeriesDataSource + Private _displayOptions As TimeSeriesDisplayOptions #End Region 'Members @@ -256,6 +257,19 @@ Public Class TimeSeries End Set End Property + ''' + ''' Options for displaying the time series + ''' + ''' + Public Property DisplayOptions As TimeSeriesDisplayOptions + Get + Return _displayOptions + End Get + Set(value As TimeSeriesDisplayOptions) + _displayOptions = value + End Set + End Property + ''' ''' Returns the start date of the time series ''' @@ -453,6 +467,7 @@ Public Class TimeSeries Me._unit = "-" Me._Interpretation = InterpretationEnum.Undefined Me.DataSource = New TimeSeriesDataSource(TimeSeriesDataSource.OriginEnum.Undefined) + Me.DisplayOptions = New TimeSeriesDisplayOptions() Me._nodes = New SortedList(Of DateTime, Double) End Sub @@ -477,6 +492,7 @@ Public Class TimeSeries target.Metadata = Me.Metadata.Copy() target._Interpretation = Me.Interpretation target.DataSource = Me.DataSource.Copy() + target.DisplayOptions = Me.DisplayOptions.Copy() Return target End Function diff --git a/source/Classes/TimeSeriesDisplayOptions.vb b/source/Classes/TimeSeriesDisplayOptions.vb new file mode 100644 index 00000000..e400731a --- /dev/null +++ b/source/Classes/TimeSeriesDisplayOptions.vb @@ -0,0 +1,169 @@ +'BlueM.Wave +'Copyright (C) BlueM Dev Group +' +' +'This program is free software: you can redistribute it and/or modify +'it under the terms of the GNU Lesser General Public License as published by +'the Free Software Foundation, either version 3 of the License, or +'(at your option) any later version. +' +'This program is distributed in the hope that it will be useful, +'but WITHOUT ANY WARRANTY; without even the implied warranty of +'MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +'GNU Lesser General Public License for more details. +' +'You should have received a copy of the GNU Lesser General Public License +'along with this program. If not, see . +' +''' +''' Class for storing time series display options +''' +Public Class TimeSeriesDisplayOptions + + Private _color As Color + Private _linestyle As Drawing2D.DashStyle + Private _linewidth As Integer + Private _showPoints As Boolean + + ''' + ''' Color + ''' + ''' Default color is empty. When empty, color is automatically assigned by the chart + ''' + Public Property Color As Color + Get + Return _color + End Get + Set(value As Color) + _color = value + End Set + End Property + + ''' + ''' Line style + ''' + ''' + Public Property LineStyle As Drawing2D.DashStyle + Get + Return _linestyle + End Get + Set(value As Drawing2D.DashStyle) + _linestyle = value + End Set + End Property + + ''' + ''' Line width + ''' + ''' + Public Property LineWidth As Integer + Get + Return _linewidth + End Get + Set(value As Integer) + _linewidth = value + End Set + End Property + + ''' + ''' Flag to show series points + ''' + ''' + Public Property ShowPoints As Boolean + Get + Return _showPoints + End Get + Set(value As Boolean) + _showPoints = value + End Set + End Property + + ''' + ''' Creates a new TimeSeriesDisplayOption instance with default properties + ''' + ''' Default Color is empty in order to allow automatic color assignment by the chart + Public Sub New() + Me.Color = Color.Empty + Me.LineStyle = Drawing2D.DashStyle.Solid + Me.LineWidth = 2 + Me.ShowPoints = False + End Sub + + ''' + ''' Returns a copy of the instance + ''' + ''' + Public Function Copy() As TimeSeriesDisplayOptions + Dim displayOptions As New TimeSeriesDisplayOptions() With { + .Color = Me.Color, + .LineStyle = Me.LineStyle, + .LineWidth = Me.LineWidth, + .ShowPoints = Me.ShowPoints + } + Return displayOptions + End Function + + ''' + ''' Sets the color from a color string + ''' + ''' + ''' Must be either the name of a known color: https://learn.microsoft.com/en-us/dotnet/api/system.drawing.knowncolor + ''' or a hexadecimal representation of ARGB values + ''' + ''' the color string + Public Sub SetColor(colorString As String) + If [Enum].IsDefined(GetType(KnownColor), colorString) Then + Me.Color = Drawing.Color.FromName(colorString) + Else + Try + 'try to convert a hexadecimal representation of ARGB values + Dim argb As Integer = Int32.Parse(colorString, Globalization.NumberStyles.HexNumber) + Me.Color = Color.FromArgb(argb) + Catch e As FormatException + Log.AddLogEntry(levels.warning, $"Color '{colorString}' is not recognized!") + End Try + End If + End Sub + + ''' + ''' Sets the line style from a string + ''' + ''' Recognized line styles: https://learn.microsoft.com/en-us/dotnet/api/system.drawing.drawing2d.dashstyle + ''' the line style as string + Public Sub SetLineStyle(lineStyle As String) + If Not [Enum].IsDefined(GetType(Drawing2D.DashStyle), lineStyle) Then + Log.AddLogEntry(levels.warning, $"Line style '{lineStyle}' is not recognized!") + Else + Me.LineStyle = [Enum].Parse(GetType(Drawing2D.DashStyle), lineStyle) + End If + End Sub + + ''' + ''' Sets the line width from a string + ''' + ''' string must be convertible to an integer + ''' line width as string + Public Sub SetLineWidth(lineWidth As String) + Dim lineWidthInt As Integer + If Not Integer.TryParse(lineWidth, lineWidthInt) Then + Log.AddLogEntry(levels.warning, $"Line width '{lineWidth}' can not be converted to integer!") + Else + Me.LineWidth = lineWidthInt + End If + End Sub + + ''' + ''' Sets the ShowPoints property from a string + ''' + ''' string must be convertible to a boolean + ''' whether to show points + Public Sub SetShowPoints(showPoints As String) + Select Case showPoints.ToLower() + Case "true", "y", "1" + Me.ShowPoints = True + Case Else + Me.ShowPoints = False + End Select + End Sub + +End Class diff --git a/source/Controllers/WaveController.vb b/source/Controllers/WaveController.vb index 9be4ca74..6d005ea0 100644 --- a/source/Controllers/WaveController.vb +++ b/source/Controllers/WaveController.vb @@ -387,20 +387,46 @@ Friend Class WaveController ''' Private Sub SaveProjectFile_Click(sender As System.Object, e As System.EventArgs) + If _model.TimeSeries.Count = 0 Then + MsgBox("No time series to save!", MsgBoxStyle.Exclamation) + Exit Sub + End If + Try + 'show save project file dialog Dim dlgres As DialogResult - - 'Prepare SaveFileDialog - View.SaveFileDialog1.Title = "Save project file" - View.SaveFileDialog1.Filter = "Wave project files (*.wvp)|*wvp" - View.SaveFileDialog1.DefaultExt = "wvp" - View.SaveFileDialog1.OverwritePrompt = True - - 'Show dialog - dlgres = View.SaveFileDialog1.ShowDialog() + Dim dlg As New SaveProjectFileDialog() + dlgres = dlg.ShowDialog() If dlgres = Windows.Forms.DialogResult.OK Then - Call _model.SaveProjectFile(View.SaveFileDialog1.FileName) + 'collect display options from chart and store them in timeseries + Dim tsList As New List(Of TimeSeries) + For Each ts As TimeSeries In _model.TimeSeries.ToList() + For Each series As Steema.TeeChart.Styles.Series In View.TChart1.Series + If series.Tag = ts.Id Then + If TypeOf series Is Steema.TeeChart.Styles.Line Then + Dim line As Steema.TeeChart.Styles.Line = CType(series, Steema.TeeChart.Styles.Line) + ts.DisplayOptions.Color = line.Color + ts.DisplayOptions.LineStyle = line.LinePen.Style + ts.DisplayOptions.LineWidth = line.LinePen.Width + ts.DisplayOptions.ShowPoints = line.Pointer.Visible + End If + Exit For + End If + Next + tsList.Add(ts) + Next + Call Fileformats.WVP.Write_File(tsList, dlg.FileName, + saveRelativePaths:=dlg.SaveRelativePaths, + saveTitle:=dlg.SaveTitle, + saveUnit:=dlg.SaveUnit, + saveInterpretation:=dlg.SaveInterpretation, + saveColor:=dlg.SaveColor, + saveLineStyle:=dlg.SaveLineStyle, + saveLineWidth:=dlg.SaveLineWidth, + savePointsVisibility:=dlg.SavePointsVisibility + ) + MsgBox($"Wave project file {dlg.FileName} saved.", MsgBoxStyle.Information) End If Catch ex As Exception @@ -2174,7 +2200,7 @@ Friend Class WaveController ''' Adds the series to the charts ''' Also adds the datasource to the MRU file list if the time series has a file datasource ''' - ''' Die anzuzeigende Zeitreihe + ''' time series to display Private Sub SeriesAdded(ts As TimeSeries) 'Check for extreme dates not supported by TChart @@ -2228,8 +2254,13 @@ Friend Class WaveController 'Namen vergeben Line1.Title = ts.Title - 'Set line width to 2 - Line1.LinePen.Width = 2 + 'set display options + If Not ts.DisplayOptions.Color.IsEmpty Then + Line1.Color = ts.DisplayOptions.Color + End If + Line1.LinePen.Style = ts.DisplayOptions.LineStyle + Line1.LinePen.Width = ts.DisplayOptions.LineWidth + Line1.Pointer.Visible = ts.DisplayOptions.ShowPoints 'Stützstellen zur Serie hinzufügen 'Main chart @@ -2287,8 +2318,12 @@ Friend Class WaveController 'Namen vergeben Line2.Title = ts.Title - 'Set line width to 2 - Line2.LinePen.Width = 2 + 'set display options + If Not ts.DisplayOptions.Color.IsEmpty Then + Line2.Color = ts.DisplayOptions.Color + End If + Line2.LinePen.Style = ts.DisplayOptions.LineStyle + Line2.LinePen.Width = ts.DisplayOptions.LineWidth 'Stützstellen zur Serie hinzufügen Line2.BeginUpdate() diff --git a/source/Dialogs/SaveProjectFileDialog.Designer.vb b/source/Dialogs/SaveProjectFileDialog.Designer.vb new file mode 100644 index 00000000..f836655f --- /dev/null +++ b/source/Dialogs/SaveProjectFileDialog.Designer.vb @@ -0,0 +1,279 @@ + +Partial Class SaveProjectFileDialog + Inherits System.Windows.Forms.Form + + 'Das Formular überschreibt den Löschvorgang, um die Komponentenliste zu bereinigen. + + Protected Overrides Sub Dispose(disposing As Boolean) + Try + If disposing AndAlso components IsNot Nothing Then + components.Dispose() + End If + Finally + MyBase.Dispose(disposing) + End Try + End Sub + + 'Wird vom Windows Form-Designer benötigt. + Private components As System.ComponentModel.IContainer + + 'Hinweis: Die folgende Prozedur ist für den Windows Form-Designer erforderlich. + 'Das Bearbeiten ist mit dem Windows Form-Designer möglich. + 'Das Bearbeiten mit dem Code-Editor ist nicht möglich. + + Private Sub InitializeComponent() + Dim resources As System.ComponentModel.ComponentResourceManager = New System.ComponentModel.ComponentResourceManager(GetType(SaveProjectFileDialog)) + Me.Button_OK = New System.Windows.Forms.Button() + Me.Button_Cancel = New System.Windows.Forms.Button() + Me.GroupBox_DisplayOptions = New System.Windows.Forms.GroupBox() + Me.CheckBox_PointsVisibility = New System.Windows.Forms.CheckBox() + Me.CheckBox_LineWidth = New System.Windows.Forms.CheckBox() + Me.CheckBox_LineStyle = New System.Windows.Forms.CheckBox() + Me.CheckBox_Color = New System.Windows.Forms.CheckBox() + Me.CheckBox_Title = New System.Windows.Forms.CheckBox() + Me.CheckBox_Unit = New System.Windows.Forms.CheckBox() + Me.CheckBox_Interpretation = New System.Windows.Forms.CheckBox() + Me.GroupBox_SeriesProperties = New System.Windows.Forms.GroupBox() + Me.TextBox_File = New System.Windows.Forms.TextBox() + Me.Button_Browse = New System.Windows.Forms.Button() + Me.Label1 = New System.Windows.Forms.Label() + Me.GroupBox_File = New System.Windows.Forms.GroupBox() + Me.GroupBox_Options = New System.Windows.Forms.GroupBox() + Me.SaveFileDialog1 = New System.Windows.Forms.SaveFileDialog() + Me.CheckBox_RelativePaths = New System.Windows.Forms.CheckBox() + Me.GroupBox_DisplayOptions.SuspendLayout() + Me.GroupBox_SeriesProperties.SuspendLayout() + Me.GroupBox_File.SuspendLayout() + Me.GroupBox_Options.SuspendLayout() + Me.SuspendLayout() + ' + 'Button_OK + ' + Me.Button_OK.Anchor = CType((System.Windows.Forms.AnchorStyles.Bottom Or System.Windows.Forms.AnchorStyles.Right), System.Windows.Forms.AnchorStyles) + Me.Button_OK.DialogResult = System.Windows.Forms.DialogResult.OK + Me.Button_OK.Location = New System.Drawing.Point(126, 246) + Me.Button_OK.Name = "Button_OK" + Me.Button_OK.Size = New System.Drawing.Size(75, 23) + Me.Button_OK.TabIndex = 1 + Me.Button_OK.Text = "Ok" + Me.Button_OK.UseVisualStyleBackColor = True + ' + 'Button_Cancel + ' + Me.Button_Cancel.Anchor = CType((System.Windows.Forms.AnchorStyles.Bottom Or System.Windows.Forms.AnchorStyles.Right), System.Windows.Forms.AnchorStyles) + Me.Button_Cancel.DialogResult = System.Windows.Forms.DialogResult.Cancel + Me.Button_Cancel.Location = New System.Drawing.Point(207, 246) + Me.Button_Cancel.Name = "Button_Cancel" + Me.Button_Cancel.Size = New System.Drawing.Size(75, 23) + Me.Button_Cancel.TabIndex = 5 + Me.Button_Cancel.Text = "Cancel" + Me.Button_Cancel.UseVisualStyleBackColor = True + ' + 'GroupBox_DisplayOptions + ' + Me.GroupBox_DisplayOptions.Controls.Add(Me.CheckBox_PointsVisibility) + Me.GroupBox_DisplayOptions.Controls.Add(Me.CheckBox_LineWidth) + Me.GroupBox_DisplayOptions.Controls.Add(Me.CheckBox_LineStyle) + Me.GroupBox_DisplayOptions.Controls.Add(Me.CheckBox_Color) + Me.GroupBox_DisplayOptions.Location = New System.Drawing.Point(138, 42) + Me.GroupBox_DisplayOptions.Name = "GroupBox_DisplayOptions" + Me.GroupBox_DisplayOptions.Size = New System.Drawing.Size(123, 120) + Me.GroupBox_DisplayOptions.TabIndex = 7 + Me.GroupBox_DisplayOptions.TabStop = False + Me.GroupBox_DisplayOptions.Text = "Display options" + ' + 'CheckBox_PointsVisibility + ' + Me.CheckBox_PointsVisibility.AutoSize = True + Me.CheckBox_PointsVisibility.Location = New System.Drawing.Point(6, 88) + Me.CheckBox_PointsVisibility.Name = "CheckBox_PointsVisibility" + Me.CheckBox_PointsVisibility.Size = New System.Drawing.Size(93, 17) + Me.CheckBox_PointsVisibility.TabIndex = 13 + Me.CheckBox_PointsVisibility.Text = "Points visibility" + Me.CheckBox_PointsVisibility.UseVisualStyleBackColor = True + ' + 'CheckBox_LineWidth + ' + Me.CheckBox_LineWidth.AutoSize = True + Me.CheckBox_LineWidth.Location = New System.Drawing.Point(6, 65) + Me.CheckBox_LineWidth.Name = "CheckBox_LineWidth" + Me.CheckBox_LineWidth.Size = New System.Drawing.Size(74, 17) + Me.CheckBox_LineWidth.TabIndex = 12 + Me.CheckBox_LineWidth.Text = "Line width" + Me.CheckBox_LineWidth.UseVisualStyleBackColor = True + ' + 'CheckBox_LineStyle + ' + Me.CheckBox_LineStyle.AutoSize = True + Me.CheckBox_LineStyle.Location = New System.Drawing.Point(6, 42) + Me.CheckBox_LineStyle.Name = "CheckBox_LineStyle" + Me.CheckBox_LineStyle.Size = New System.Drawing.Size(70, 17) + Me.CheckBox_LineStyle.TabIndex = 8 + Me.CheckBox_LineStyle.Text = "Line style" + Me.CheckBox_LineStyle.UseVisualStyleBackColor = True + ' + 'CheckBox_Color + ' + Me.CheckBox_Color.AutoSize = True + Me.CheckBox_Color.Location = New System.Drawing.Point(6, 19) + Me.CheckBox_Color.Name = "CheckBox_Color" + Me.CheckBox_Color.Size = New System.Drawing.Size(72, 17) + Me.CheckBox_Color.TabIndex = 7 + Me.CheckBox_Color.Text = "Line color" + Me.CheckBox_Color.UseVisualStyleBackColor = True + ' + 'CheckBox_Title + ' + Me.CheckBox_Title.AutoSize = True + Me.CheckBox_Title.Location = New System.Drawing.Point(6, 19) + Me.CheckBox_Title.Name = "CheckBox_Title" + Me.CheckBox_Title.Size = New System.Drawing.Size(46, 17) + Me.CheckBox_Title.TabIndex = 9 + Me.CheckBox_Title.Text = "Title" + Me.CheckBox_Title.UseVisualStyleBackColor = True + ' + 'CheckBox_Unit + ' + Me.CheckBox_Unit.AutoSize = True + Me.CheckBox_Unit.Location = New System.Drawing.Point(6, 42) + Me.CheckBox_Unit.Name = "CheckBox_Unit" + Me.CheckBox_Unit.Size = New System.Drawing.Size(45, 17) + Me.CheckBox_Unit.TabIndex = 10 + Me.CheckBox_Unit.Text = "Unit" + Me.CheckBox_Unit.UseVisualStyleBackColor = True + ' + 'CheckBox_Interpretation + ' + Me.CheckBox_Interpretation.AutoSize = True + Me.CheckBox_Interpretation.Location = New System.Drawing.Point(6, 65) + Me.CheckBox_Interpretation.Name = "CheckBox_Interpretation" + Me.CheckBox_Interpretation.Size = New System.Drawing.Size(88, 17) + Me.CheckBox_Interpretation.TabIndex = 11 + Me.CheckBox_Interpretation.Text = "Interpretation" + Me.CheckBox_Interpretation.UseVisualStyleBackColor = True + ' + 'GroupBox_SeriesProperties + ' + Me.GroupBox_SeriesProperties.Controls.Add(Me.CheckBox_Title) + Me.GroupBox_SeriesProperties.Controls.Add(Me.CheckBox_Unit) + Me.GroupBox_SeriesProperties.Controls.Add(Me.CheckBox_Interpretation) + Me.GroupBox_SeriesProperties.Location = New System.Drawing.Point(9, 42) + Me.GroupBox_SeriesProperties.Name = "GroupBox_SeriesProperties" + Me.GroupBox_SeriesProperties.Size = New System.Drawing.Size(123, 120) + Me.GroupBox_SeriesProperties.TabIndex = 8 + Me.GroupBox_SeriesProperties.TabStop = False + Me.GroupBox_SeriesProperties.Text = "Series properties" + ' + 'TextBox_File + ' + Me.TextBox_File.Anchor = CType(((System.Windows.Forms.AnchorStyles.Top Or System.Windows.Forms.AnchorStyles.Left) _ + Or System.Windows.Forms.AnchorStyles.Right), System.Windows.Forms.AnchorStyles) + Me.TextBox_File.Location = New System.Drawing.Point(62, 15) + Me.TextBox_File.Name = "TextBox_File" + Me.TextBox_File.Size = New System.Drawing.Size(162, 20) + Me.TextBox_File.TabIndex = 9 + ' + 'Button_Browse + ' + Me.Button_Browse.Anchor = CType((System.Windows.Forms.AnchorStyles.Top Or System.Windows.Forms.AnchorStyles.Right), System.Windows.Forms.AnchorStyles) + Me.Button_Browse.Location = New System.Drawing.Point(230, 14) + Me.Button_Browse.Name = "Button_Browse" + Me.Button_Browse.Size = New System.Drawing.Size(34, 23) + Me.Button_Browse.TabIndex = 10 + Me.Button_Browse.Text = "..." + Me.Button_Browse.UseVisualStyleBackColor = True + ' + 'Label1 + ' + Me.Label1.AutoSize = True + Me.Label1.Location = New System.Drawing.Point(6, 19) + Me.Label1.Name = "Label1" + Me.Label1.Size = New System.Drawing.Size(50, 13) + Me.Label1.TabIndex = 11 + Me.Label1.Text = "File path:" + ' + 'GroupBox_File + ' + Me.GroupBox_File.Anchor = CType(((System.Windows.Forms.AnchorStyles.Top Or System.Windows.Forms.AnchorStyles.Left) _ + Or System.Windows.Forms.AnchorStyles.Right), System.Windows.Forms.AnchorStyles) + Me.GroupBox_File.Controls.Add(Me.Label1) + Me.GroupBox_File.Controls.Add(Me.TextBox_File) + Me.GroupBox_File.Controls.Add(Me.Button_Browse) + Me.GroupBox_File.Location = New System.Drawing.Point(12, 12) + Me.GroupBox_File.Name = "GroupBox_File" + Me.GroupBox_File.Size = New System.Drawing.Size(270, 46) + Me.GroupBox_File.TabIndex = 12 + Me.GroupBox_File.TabStop = False + ' + 'GroupBox_Options + ' + Me.GroupBox_Options.Anchor = CType((((System.Windows.Forms.AnchorStyles.Top Or System.Windows.Forms.AnchorStyles.Bottom) _ + Or System.Windows.Forms.AnchorStyles.Left) _ + Or System.Windows.Forms.AnchorStyles.Right), System.Windows.Forms.AnchorStyles) + Me.GroupBox_Options.Controls.Add(Me.CheckBox_RelativePaths) + Me.GroupBox_Options.Controls.Add(Me.GroupBox_SeriesProperties) + Me.GroupBox_Options.Controls.Add(Me.GroupBox_DisplayOptions) + Me.GroupBox_Options.Location = New System.Drawing.Point(12, 64) + Me.GroupBox_Options.Name = "GroupBox_Options" + Me.GroupBox_Options.Size = New System.Drawing.Size(270, 171) + Me.GroupBox_Options.TabIndex = 13 + Me.GroupBox_Options.TabStop = False + Me.GroupBox_Options.Text = "Options" + ' + 'CheckBox_RelativePaths + ' + Me.CheckBox_RelativePaths.AutoSize = True + Me.CheckBox_RelativePaths.Location = New System.Drawing.Point(15, 19) + Me.CheckBox_RelativePaths.Name = "CheckBox_RelativePaths" + Me.CheckBox_RelativePaths.Size = New System.Drawing.Size(117, 17) + Me.CheckBox_RelativePaths.TabIndex = 9 + Me.CheckBox_RelativePaths.Text = "Save relative paths" + Me.CheckBox_RelativePaths.UseVisualStyleBackColor = True + ' + 'SaveProjectFileDialog + ' + Me.AutoScaleDimensions = New System.Drawing.SizeF(6.0!, 13.0!) + Me.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font + Me.ClientSize = New System.Drawing.Size(294, 281) + Me.Controls.Add(Me.GroupBox_Options) + Me.Controls.Add(Me.GroupBox_File) + Me.Controls.Add(Me.Button_OK) + Me.Controls.Add(Me.Button_Cancel) + Me.Icon = CType(resources.GetObject("$this.Icon"), System.Drawing.Icon) + Me.MaximizeBox = False + Me.MinimizeBox = False + Me.MinimumSize = New System.Drawing.Size(310, 320) + Me.Name = "SaveProjectFileDialog" + Me.ShowInTaskbar = False + Me.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent + Me.Text = "Save project file" + Me.GroupBox_DisplayOptions.ResumeLayout(False) + Me.GroupBox_DisplayOptions.PerformLayout() + Me.GroupBox_SeriesProperties.ResumeLayout(False) + Me.GroupBox_SeriesProperties.PerformLayout() + Me.GroupBox_File.ResumeLayout(False) + Me.GroupBox_File.PerformLayout() + Me.GroupBox_Options.ResumeLayout(False) + Me.GroupBox_Options.PerformLayout() + Me.ResumeLayout(False) + + End Sub + Friend WithEvents Button_OK As System.Windows.Forms.Button + Private WithEvents Button_Cancel As System.Windows.Forms.Button + Friend WithEvents GroupBox_DisplayOptions As GroupBox + Friend WithEvents CheckBox_PointsVisibility As CheckBox + Friend WithEvents CheckBox_LineWidth As CheckBox + Friend WithEvents CheckBox_LineStyle As CheckBox + Friend WithEvents CheckBox_Color As CheckBox + Friend WithEvents CheckBox_Title As CheckBox + Friend WithEvents CheckBox_Unit As CheckBox + Friend WithEvents CheckBox_Interpretation As CheckBox + Friend WithEvents GroupBox_SeriesProperties As GroupBox + Friend WithEvents TextBox_File As TextBox + Friend WithEvents Button_Browse As Button + Friend WithEvents Label1 As Label + Friend WithEvents GroupBox_File As GroupBox + Friend WithEvents GroupBox_Options As GroupBox + Friend WithEvents SaveFileDialog1 As SaveFileDialog + Friend WithEvents CheckBox_RelativePaths As CheckBox +End Class diff --git a/source/Dialogs/SaveProjectFileDialog.resx b/source/Dialogs/SaveProjectFileDialog.resx new file mode 100644 index 00000000..6f29b173 --- /dev/null +++ b/source/Dialogs/SaveProjectFileDialog.resx @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + 17, 17 + + + + + AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAAAABMLAAATCwAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAALRpK/+zaCv/smgq/7FoKv+wZir/r2Yq/69mKf+uZSn/rmUo/61kKP+tZCj/rGQo/6tj + KP+rYyj/AAAAAAAAAAC2ai3/KWgyTydoMUY5UCdFJ2gxCQAAAABIKBMdAAAAAAAAAABCJRIdAAAAACdv + MhosWihoKHM1DAAAAAAAAAAAt2st/yx0ORkrdTiZLXU34it1OO0sdjhoAAAAAAAAAAAAAAAAAAAAACt/ + OAgsgjjeK4Q2kwAAAAAAAAAAAAAAALltLv8oFZxAAAAAAAAAAAAyiEGWMolB8TGKQKIxjEAMAAAAAAAA + AAAxj0CIL5I+7wAAAAAAAAAAAAAAAAAAAAC7bi//BAD5VAQA+xcAAAAAAAAAADmZSko5mUjoN5pI4Dib + Rq02nUW+NZ9E9DOgQl8AAAAAEQ39GgAAAAAAAAAAvW8w/wUB/08GAv/cAAAAAAAAAAAAAAAA3FAAd0Ci + Spc8pEvMOqRJzTejRlcAAAAAAAAAABsX+7AAAAAAAAAAAL5xMP9QMHsvEQ39/hIO/IbgWACE4FcA/+BX + Av3gVwP+31YGVwAAAAAAAAAAAAAAAAAAAAAkIfn6AAAAAAAAAADAcjL/AAAAABoW+6UcGfn7uk499+Bh + CpLhYg0D32IQuN5hEfjdYhV+AAAAAAAAAAAtKvnALiv38AAAAAAAAAAAwnMy/wAAAAAAAAAAOSrr/kUx + 4fkAAAAAAAAAAAAAAADgbyCo3nIk+95zJ581MvclNjP2/4hbppcAAAAAAAAAAMNzM/+ARyId4XYbiq9g + Z/o4MvX3OTb2lQAAAAAAAAAAAAAAAOB/M43fgTf934I5+9+GPPnfiD7+AAAAAAAAAADEdDT/4oAmA+B/ + LPnggTC2QT71h0NA9P5GQ/M8AAAAAAAAAABJRvMiS0jx8VBK7vbgk0uAAAAAAAAAAAAAAAAAxXU0/+CH + ODPeiT/93oxBCQAAAABQTfHDUk/y+lJP8spTUfDdU1Hw+1NR8OJTUfAiAAAAAAAAAAAAAAAAAAAAAMZ2 + NP/GfT9m3ZJPpwAAAAAAAAAAAAAAAFpY74paWO/gWVfv01lX714AAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AADGdjT/3ZdbVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAA//8AAIABAAC//wAAo/MAALjnAAC+DwAArx0AAKB9AAChOQAAs4kAAKHBAACh4wAArA8AAK4/ + //+//////////w== + + + \ No newline at end of file diff --git a/source/Dialogs/SaveProjectFileDialog.vb b/source/Dialogs/SaveProjectFileDialog.vb new file mode 100644 index 00000000..15e43a3f --- /dev/null +++ b/source/Dialogs/SaveProjectFileDialog.vb @@ -0,0 +1,125 @@ +'BlueM.Wave +'Copyright (C) BlueM Dev Group +' +' +'This program is free software: you can redistribute it and/or modify +'it under the terms of the GNU Lesser General Public License as published by +'the Free Software Foundation, either version 3 of the License, or +'(at your option) any later version. +' +'This program is distributed in the hope that it will be useful, +'but WITHOUT ANY WARRANTY; without even the implied warranty of +'MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +'GNU Lesser General Public License for more details. +' +'You should have received a copy of the GNU Lesser General Public License +'along with this program. If not, see . +' +Friend Class SaveProjectFileDialog + + Public Sub New() + + ' This call is required by the Windows Form Designer. + InitializeComponent() + + ' Add any initialization after the InitializeComponent() call. + + End Sub + + Friend ReadOnly Property FileName As String + Get + Return TextBox_File.Text + End Get + End Property + + Friend ReadOnly Property SaveRelativePaths As Boolean + Get + Return CheckBox_RelativePaths.Checked + End Get + End Property + + Friend ReadOnly Property SaveTitle As Boolean + Get + Return CheckBox_Title.Checked + End Get + End Property + + Friend ReadOnly Property SaveUnit As Boolean + Get + Return CheckBox_Unit.Checked + End Get + End Property + + Friend ReadOnly Property SaveInterpretation As Boolean + Get + Return CheckBox_Interpretation.Checked + End Get + End Property + + Friend ReadOnly Property SaveColor As Boolean + Get + Return CheckBox_Color.Checked + End Get + End Property + + Friend ReadOnly Property SaveLineStyle As Boolean + Get + Return CheckBox_LineStyle.Checked + End Get + End Property + + Friend ReadOnly Property SaveLineWidth As Boolean + Get + Return CheckBox_LineWidth.Checked + End Get + End Property + + Friend ReadOnly Property SavePointsVisibility As Boolean + Get + Return CheckBox_PointsVisibility.Checked + End Get + End Property + + ''' + ''' OK button pressed + ''' + ''' + ''' + ''' + Private Sub Button_OK_Click(sender As System.Object, e As System.EventArgs) Handles Button_OK.Click + + 'validate inputs + If Me.FileName.Length = 0 Then + MsgBox("Please specify a file name!", MsgBoxStyle.Exclamation) + Me.DialogResult = DialogResult.None + End If + + 'prompt for overwrite + If IO.File.Exists(Me.FileName) Then + Dim response As MsgBoxResult = MsgBox($"Replace existing file {Me.FileName}?", MsgBoxStyle.YesNo Or MsgBoxStyle.Exclamation) + If Not response = MsgBoxResult.Yes Then + Me.DialogResult = DialogResult.None + End If + End If + + End Sub + + Private Sub Button_Browse_Click(sender As Object, e As EventArgs) Handles Button_Browse.Click + + Dim dlgres As DialogResult + + 'Prepare SaveFileDialog + SaveFileDialog1.Title = "Save project file" + SaveFileDialog1.Filter = "Wave project files (*.wvp)|*.wvp" + SaveFileDialog1.DefaultExt = "wvp" + SaveFileDialog1.OverwritePrompt = False 'will be prompted after OK button is pressed + + 'Show dialog + dlgres = SaveFileDialog1.ShowDialog() + + If dlgres = DialogResult.OK Then + Me.TextBox_File.Text = SaveFileDialog1.FileName + End If + + End Sub +End Class \ No newline at end of file diff --git a/source/FileFormats/WVP.vb b/source/FileFormats/WVP.vb index 4d1f764b..a75c2b0c 100644 --- a/source/FileFormats/WVP.vb +++ b/source/FileFormats/WVP.vb @@ -27,8 +27,21 @@ Namespace Fileformats Private projectfile As String - 'fileDict = {filename1:{series1:title1, series2:title2, ...}, ...} - Private fileDict As Dictionary(Of String, Dictionary(Of String, String)) + ''' + ''' Structure for storing series options + ''' + Private Structure seriesOption + Dim title As String + Dim unit As String + Dim color As String + Dim linestyle As String + Dim linewidth As String + Dim interpretation As String + Dim showpoints As String + End Structure + + 'fileDict = {filename1:{series1:options1, series2:options2, ...}, ...} + Private fileDict As Dictionary(Of String, Dictionary(Of String, seriesOption)) 'settingsDict = {filename1:{setting1:value1, setting2:value2, ...}, ...} Private settingsDict As Dictionary(Of String, Dictionary(Of String, String)) @@ -37,7 +50,7 @@ Namespace Fileformats Me.projectfile = file - fileDict = New Dictionary(Of String, Dictionary(Of String, String)) + fileDict = New Dictionary(Of String, Dictionary(Of String, seriesOption)) settingsDict = New Dictionary(Of String, Dictionary(Of String, String)) Call Me.Parse() @@ -51,7 +64,7 @@ Namespace Fileformats Dim fstr As IO.FileStream Dim strRead As IO.StreamReader - Dim line, parts(), file, series, title As String + Dim line, parts(), file, seriesName As String Log.AddLogEntry(Log.levels.info, $"Parsing Wave project file {projectfile} ...") @@ -62,12 +75,13 @@ Namespace Fileformats 'file=path\to\file1 ' series=seriesname1 ' series=series2: "optional title" + ' series=series3: title=custom title, color=Red, linestyle=Dash, unit=mm, interpretation=BlockRight 'file=path\to\file2 - ' series=series3 ' series=series4 + ' series=series5 ' fstr = New IO.FileStream(projectfile, IO.FileMode.Open) - strRead = New IO.StreamReader(fstr, detectEncodingFromByteOrderMarks:=True) + strRead = New IO.StreamReader(fstr, Text.Encoding.UTF8) file = "" @@ -90,63 +104,107 @@ Namespace Fileformats file = IO.Path.GetFullPath(IO.Path.Combine(IO.Path.GetDirectoryName(projectfile), file)) End If If Not fileDict.ContainsKey(file) Then - fileDict.Add(file, New Dictionary(Of String, String)) + fileDict.Add(file, New Dictionary(Of String, seriesOption)) End If ElseIf line.ToLower().StartsWith("series=") Then 'series - line = line.Split("=".ToCharArray(), 2)(1).Trim() + line = line.Split("=".ToCharArray(), 2).Last.Trim() 'series name may be enclosed in quotes and be followed by an optional title, which may also be enclosed in quotes 'examples: 'series 'series:title '"se:ries":title '"se:ries":"title" + 'series:title="title", unit=m³/s, color=Red, linestyle=Dash, linewidth=3, interpretation=BlockRight Dim pattern As String If line.StartsWith("""") Then 'series name is enclosed in quotes - pattern = "^""([^""]+)""(:(.+))?$" + pattern = "^""(?[^""]+)""(?:(?.+))?$" Else 'no quotes around series name - pattern = "^([^:]+)(:(.+))?$" + pattern = "^(?[^:]+)(?:(?.+))?$" End If Dim m As Match = Regex.Match(line, pattern) If m.Success Then - series = m.Groups(1).Value.Trim() - If m.Groups(2).Success Then - title = m.Groups(3).Value.Replace("""", "").Trim() 'remove quotes around title here - Else - title = "" - End If - 'add series to file - If fileDict.ContainsKey(file) Then - If Not fileDict(file).ContainsKey(series) Then - fileDict(file).Add(series, title) + seriesName = m.Groups("name").Value.Trim() + 'check for additional series options + 'by default, series options are nothing + Dim seriesOptions As New seriesOption With { + .title = Nothing, + .unit = Nothing, + .color = Nothing, + .linestyle = Nothing, + .linewidth = Nothing, + .interpretation = Nothing + } + If m.Groups("options").Success Then + 'parse series options + Dim optionString As String = m.Groups("optionstring").Value.Trim() + 'remove quoted parts + Dim stripped As String = Regex.Replace(optionString, "(?"").+?(?<-q>"")", "") + If Not stripped.Contains("=") Then + 'no keyword options, title only + seriesOptions.title = optionString.Replace("""", "").Trim() 'remove any quotes around title + Else + 'keyword options, comma-separated, values may be quoted + Dim matches As MatchCollection = Regex.Matches(optionString, "(?[^"",=]+)\s?=\s?(?(?"").+?(?<-q>"")|[^"",=]+),?") + For Each m In matches + Dim keyword As String = m.Groups("key").Value.ToLower().Trim() + Dim value As String = m.Groups("value").Value.Replace("""", "").Trim() 'values are case-sensitive! + Select Case keyword + Case "title" + seriesOptions.title = value + Case "unit" + seriesOptions.unit = value + Case "color" + seriesOptions.color = value + Case "linestyle" + seriesOptions.linestyle = value + Case "linewidth" + seriesOptions.linewidth = value + Case "interpretation" + seriesOptions.interpretation = value + Case "showpoints" + seriesOptions.showpoints = value + Case Else + Log.AddLogEntry(levels.warning, $"Series import option keyword {keyword} not recognized!") + End Select + Next + End If + End If + 'add series to fileDict + If fileDict.ContainsKey(file) Then + If Not fileDict(file).ContainsKey(seriesName) Then + fileDict(file).Add(seriesName, seriesOptions) Else - Log.AddLogEntry(Log.levels.warning, $"Series {series} is specified more than once, only the first mention will be processed!") + Log.AddLogEntry(Log.levels.warning, $"Series {seriesName} is specified more than once, only the first mention will be processed!") End If Else - Log.AddLogEntry(Log.levels.warning, $"Series {series} is not associated with a file and will be ignored!") + Log.AddLogEntry(Log.levels.warning, $"Series {seriesName} is not associated with a file and will be ignored!") End If Else Log.AddLogEntry(Log.levels.warning, $"Unable to parse series definition 'series={line}', this series will be ignored!") End If ElseIf line.Contains("=") Then - 'settings + 'file import settings + Dim key, value As String parts = line.Trim().Split("=".ToCharArray(), 2) - 'add setting to file + key = parts(0).Trim() + value = parts(1).Trim() + 'add setting to fileDict If fileDict.ContainsKey(file) Then If Not settingsDict.ContainsKey(file) Then settingsDict.Add(file, New Dictionary(Of String, String)) End If - If Not settingsDict(file).ContainsKey(parts(0)) Then - settingsDict(file).Add(parts(0), parts(1)) + If Not settingsDict(file).ContainsKey(key) Then + settingsDict(file).Add(key, value) Else - Log.AddLogEntry(Log.levels.warning, $"Setting {parts(0)} is specified more than once, only the first mention will be processed!") + Log.AddLogEntry(Log.levels.warning, $"Setting {key} is specified more than once, only the first mention will be processed!") End If Else - Log.AddLogEntry(Log.levels.warning, $"Setting {parts(0)} is not associated with a file and will be ignored!") + Log.AddLogEntry(Log.levels.warning, $"Setting {key} is not associated with a file and will be ignored!") End If Else @@ -168,7 +226,7 @@ Namespace Fileformats Dim found As Boolean Dim value As String - Dim seriesList As Dictionary(Of String, String) + Dim series As Dictionary(Of String, seriesOption) Dim seriesNotFound As List(Of String) Dim fileInstance As TimeSeriesFile @@ -182,12 +240,12 @@ Namespace Fileformats 'get an instance of the file fileInstance = TimeSeriesFile.getInstance(file) - 'apply custom import settings + 'apply custom file import settings If settingsDict.ContainsKey(file) Then - For Each setting As String In settingsDict(file).Keys - value = settingsDict(file)(setting) + For Each key As String In settingsDict(file).Keys + value = settingsDict(file)(key) Try - Select Case setting.ToLower() + Select Case key.ToLower() Case "iscolumnseparated" fileInstance.IsColumnSeparated = If(value.ToLower() = "true", True, False) Case "separator" @@ -209,38 +267,38 @@ Namespace Fileformats Case "datetimecolumnindex" fileInstance.DateTimeColumnIndex = Convert.ToInt32(value) Case Else - Log.AddLogEntry(Log.levels.warning, $"Setting '{setting}' was not recognized and was ignored!") + Log.AddLogEntry(Log.levels.warning, $"Setting '{key}' was not recognized and was ignored!") End Select Catch ex As Exception - Log.AddLogEntry(Log.levels.warning, $"Setting '{setting}' with value '{value}' could not be parsed and was ignored!") + Log.AddLogEntry(Log.levels.warning, $"Setting '{key}' with value '{value}' could not be parsed and was ignored!") End Try Next 'reread columns with new settings fileInstance.readSeriesInfo() End If - 'get the list of series to be imported - seriesList = fileDict(file) + 'get the series to be imported + series = fileDict(file) 'select series for importing - If seriesList.Count = 0 Then + If series.Count = 0 Then 'read all series contained in the file Call fileInstance.selectAllSeries() Else 'loop over series names seriesNotFound = New List(Of String) - For Each series In seriesList.Keys - found = fileInstance.selectSeries(series) + For Each seriesName As String In series.Keys + found = fileInstance.selectSeries(seriesName) If Not found Then - seriesNotFound.Add(series) + seriesNotFound.Add(seriesName) End If Next 'remove series that were not found from the dictionary - For Each series In seriesNotFound - seriesList.Remove(series) + For Each seriesName As String In seriesNotFound + series.Remove(seriesName) Next 'if no series remain to be imported, abort reading the file altogether - If seriesList.Count = 0 Then + If series.Count = 0 Then Log.AddLogEntry(Log.levels.error, "No series left to import, skipping file!") Continue For End If @@ -255,10 +313,35 @@ Namespace Fileformats 'store the series For Each ts As TimeSeries In fileInstance.TimeSeries.Values - 'change title if specified in the project file - If seriesList.ContainsKey(ts.Title) Then - If seriesList(ts.Title) <> "" Then - ts.Title = seriesList(ts.Title) + 'set series options + If series.ContainsKey(ts.Title) Then + Dim seriesOptions As seriesOption = series(ts.Title) + 'options affecting the time series itself + If Not IsNothing(seriesOptions.title) Then + ts.Title = seriesOptions.title + End If + If Not IsNothing(seriesOptions.unit) Then + ts.Unit = seriesOptions.unit + End If + If Not IsNothing(seriesOptions.interpretation) Then + If Not [Enum].IsDefined(GetType(TimeSeries.InterpretationEnum), seriesOptions.interpretation) Then + Log.AddLogEntry(levels.warning, $"Interpretation {seriesOptions.interpretation} is not recognized!") + Else + ts.Interpretation = [Enum].Parse(GetType(TimeSeries.InterpretationEnum), seriesOptions.interpretation) + End If + End If + 'display options + If Not IsNothing(seriesOptions.color) Then + ts.DisplayOptions.SetColor(seriesOptions.color) + End If + If Not IsNothing(seriesOptions.linestyle) Then + ts.DisplayOptions.SetLineStyle(seriesOptions.linestyle) + End If + If Not IsNothing(seriesOptions.linewidth) Then + ts.DisplayOptions.SetLineWidth(seriesOptions.linewidth) + End If + If Not IsNothing(seriesOptions.showpoints) Then + ts.DisplayOptions.SetShowPoints(seriesOptions.showpoints) End If End If tsList.Add(ts) @@ -269,6 +352,135 @@ Namespace Fileformats End Function + ''' + ''' Write a Wave project file + ''' + ''' Only Timeseries with `.DataSource.Origin = TimeSeriesDataSource.OriginEnum.FileImport` will be saved + ''' List of Timeseries to save + ''' Path to the wvp file to write + ''' Whether to save relative paths + ''' Whether to save titles + ''' Whether to save units + ''' Whether to save interpretations + ''' Whether to save line colors + ''' Whether to save line styles + ''' Whether to save line widths + ''' Whether to save points visibility + Public Shared Sub Write_File(ByRef tsList As List(Of TimeSeries), file As String, + Optional saveRelativePaths As Boolean = False, + Optional saveTitle As Boolean = False, + Optional saveUnit As Boolean = False, + Optional saveInterpretation As Boolean = False, + Optional saveColor As Boolean = False, + Optional saveLineStyle As Boolean = False, + Optional saveLineWidth As Boolean = False, + Optional savePointsVisibility As Boolean = False + ) + + 'check whether there are any series with a file datasource at all + Dim haveFileDatasources As Boolean = False + For Each ts As TimeSeries In tsList + If ts.DataSource.Origin = TimeSeriesDataSource.OriginEnum.FileImport Then + haveFileDatasources = True + Exit For + End If + Next + If Not haveFileDatasources Then + Dim msg As String = $"None of the series originate from a file import! No project file was saved! Save the chart with data or export the time series to preserve them!" + Log.AddLogEntry(Log.levels.error, msg) + Throw New Exception(msg) + End If + + 'check if title only + Dim saveTitleOnly As Boolean = Not {saveUnit, saveInterpretation, saveColor, saveLineStyle, saveLineWidth, savePointsVisibility}.Contains(True) + + 'keep a list of series that could not be saved + Dim unsavedSeries As New List(Of String) + + 'write the project file + Dim fs As New IO.FileStream(file, IO.FileMode.Create, IO.FileAccess.Write) + Dim strwrite As New IO.StreamWriter(fs, Text.Encoding.UTF8) + + strwrite.WriteLine("# Wave project file") + + 'loop over series and save to file + Dim filePath As String + Dim lastFilePath As String = "" + For Each ts As TimeSeries In tsList + If Not ts.DataSource.Origin = TimeSeriesDataSource.OriginEnum.FileImport Then + unsavedSeries.Add(ts.Title) + Log.AddLogEntry(Log.levels.warning, $"Series '{ts.Title}' with datasource {ts.DataSource} does not originate from a file import and could not be saved to the project file!") + Else + filePath = ts.DataSource.FilePath + If saveRelativePaths Then + filePath = Helpers.GetRelativePath(file, filePath) + End If + If filePath <> lastFilePath Then + 'write new file path + strwrite.WriteLine("file=" & filePath) + lastFilePath = filePath + End If + 'write series name + Dim line As String + Dim seriesName As String = ts.DataSource.Title + If seriesName.Contains(":") Then + 'enclose series names containing ":" in quotes + seriesName = $"""{seriesName}""" + End If + line = $" series={seriesName}" + + 'options + Dim options As New List(Of String) + 'series options + If saveTitle Then + If ts.Title <> seriesName Then + If saveTitleOnly Then + options.Add($"""{ts.Title}""") + Else + options.Add($"title=""{ts.Title}""") + End If + End If + End If + If saveUnit Then + options.Add($"unit=""{ts.Unit}""") + End If + If saveInterpretation Then + options.Add($"interpretation={ts.Interpretation}") + End If + 'display options + If saveColor Then + options.Add($"color={ts.DisplayOptions.Color.Name}") + End If + If saveLineWidth Then + options.Add($"linewidth={ts.DisplayOptions.LineWidth}") + End If + If saveLineStyle Then + options.Add($"linestyle={ts.DisplayOptions.LineStyle}") + End If + If savePointsVisibility Then + options.Add($"showpoints={ts.DisplayOptions.ShowPoints}") + End If + + If options.Count > 0 Then + line &= ": " & String.Join(", ", options) + End If + strwrite.WriteLine(line) + End If + Next + + strwrite.Close() + fs.Close() + + If unsavedSeries.Count = 0 Then + Log.AddLogEntry(Log.levels.info, $"Wave project file {file} saved.") + Else + Dim msg As String = $"Wave project file {file} saved. {unsavedSeries.Count} series could not be saved! Save the chart with data or export the time series to preserve them!" + Log.AddLogEntry(Log.levels.warning, msg) + Throw New Exception(msg) + End If + + End Sub + End Class End Namespace \ No newline at end of file diff --git a/source/Helpers.vb b/source/Helpers.vb index d0290492..5e3c5f31 100644 --- a/source/Helpers.vb +++ b/source/Helpers.vb @@ -304,4 +304,52 @@ Public Module Helpers Down End Enum + ''' + '''Creates a relative path from one file or folder to another. + ''' + '''Contains the directory that defines the start of the relative path. + '''Contains the path that defines the endpoint of the relative path. + '''The relative path from the start directory to the end path. + ''' or is null. + ''' + ''' + ''' + '''This function is a replacement for IO.Path.GetRelativePath() which is only available in later .NET versions. + '''Based on C# code from https://stackoverflow.com/a/32113484 + ''' + Public Function GetRelativePath(fromPath As String, toPath As String) As String + + If String.IsNullOrEmpty(fromPath) Then + Throw New ArgumentNullException("fromPath") + End If + If String.IsNullOrEmpty(toPath) Then + Throw New ArgumentNullException("toPath") + End If + + Dim fromUri As Uri = New Uri(AppendDirectorySeparatorChar(fromPath)) + Dim toUri As Uri = New Uri(AppendDirectorySeparatorChar(toPath)) + + If fromUri.Scheme <> toUri.Scheme Then + Return toPath + End If + + Dim relativeUri As Uri = fromUri.MakeRelativeUri(toUri) + Dim relativePath As String = Uri.UnescapeDataString(relativeUri.ToString()) + + If String.Equals(toUri.Scheme, Uri.UriSchemeFile, StringComparison.OrdinalIgnoreCase) Then + relativePath = relativePath.Replace(IO.Path.AltDirectorySeparatorChar, IO.Path.DirectorySeparatorChar) + End If + + Return relativePath + End Function + + Private Function AppendDirectorySeparatorChar(path As String) As String + 'Append a slash only if the path is a directory and does not have a slash. + If Not IO.Path.HasExtension(path) And Not path.EndsWith(IO.Path.DirectorySeparatorChar.ToString()) Then + Return path + IO.Path.DirectorySeparatorChar + End If + + Return path + End Function + End Module diff --git a/source/Wave.vb b/source/Wave.vb index 4324bd74..8d0f3c23 100644 --- a/source/Wave.vb +++ b/source/Wave.vb @@ -443,64 +443,6 @@ Public Class Wave RaiseEvent SeriesAllReordered() End Sub - Friend Sub SaveProjectFile(projectfile As String) - - 'collect datasources - Dim datasources As New Dictionary(Of String, List(Of String)) '{file: [title, ...], ...} - Dim unsavedSeries As New List(Of String) - Dim file, title As String - For Each ts As TimeSeries In Me.TimeSeries.Values - If ts.DataSource.Origin = TimeSeriesDataSource.OriginEnum.FileImport Then - file = ts.DataSource.FilePath - title = ts.DataSource.Title - If Not datasources.ContainsKey(file) Then - datasources.Add(file, New List(Of String)) - End If - datasources(file).Add(title) - Else - unsavedSeries.Add(ts.Title) - Log.AddLogEntry(Log.levels.warning, $"Series '{ts.Title}' with datasource {ts.DataSource} does not originate from a file import and could not be saved to the project file!") - End If - Next - - If datasources.Count = 0 Then - Dim msg As String = $"None of the series originate from a file import! No project file was saved! Save the chart with data or export the time series to preserve them!" - Log.AddLogEntry(Log.levels.error, msg) - Throw New Exception(msg) - End If - - 'write the project file - Dim fs As New IO.FileStream(projectfile, IO.FileMode.Create, IO.FileAccess.Write) - Dim strwrite As New IO.StreamWriter(fs, Helpers.DefaultEncoding) - - strwrite.WriteLine("# Wave project file") - - For Each file In datasources.Keys - 'TODO: write relative paths to the project file? - strwrite.WriteLine("file=" & file) - For Each title In datasources(file) - 'TODO: if a series was renamed, write the new title to the project file - If title.Contains(":") Then - 'enclose titles containing ":" in quotes - title = $"""{title}""" - End If - strwrite.WriteLine(" series=" & title) - Next - Next - - strwrite.Close() - fs.Close() - - If unsavedSeries.Count = 0 Then - Log.AddLogEntry(Log.levels.info, $"Wave project file {projectfile} saved.") - Else - Dim msg As String = $"Wave project file {projectfile} saved. {unsavedSeries.Count} series could not be saved! Save the chart with data or export the time series to preserve them!" - Log.AddLogEntry(Log.levels.warning, msg) - Throw New Exception(msg) - End If - - End Sub - ''' ''' Initiates timeseries export ''' diff --git a/source/Wave.vbproj b/source/Wave.vbproj index 8ec34118..f25527d7 100644 --- a/source/Wave.vbproj +++ b/source/Wave.vbproj @@ -202,12 +202,19 @@ Form + + SaveProjectFileDialog.vb + + + Form + SelectSeriesDialog.vb Form + @@ -420,6 +427,9 @@ CalculatorDialog.vb + + SaveProjectFileDialog.vb + ExportDiag.vb diff --git a/tests/TestData.vb b/tests/TestData.vb index af17faa0..bf923ffa 100644 --- a/tests/TestData.vb +++ b/tests/TestData.vb @@ -25,7 +25,7 @@ Module TestData ''' which is expected to be in the same directory as BlueM.Wave ''' ''' - Private Function getTestDataDir() As String + Friend Function getTestDataDir() As String Try Dim appdir As String = My.Application.Info.DirectoryPath() 'e.g. BlueM.Wave\tests\bin\x64\Debug Dim testdatadir As String = appdir diff --git a/tests/TestWVP.vb b/tests/TestWVP.vb new file mode 100644 index 00000000..b12e96d9 --- /dev/null +++ b/tests/TestWVP.vb @@ -0,0 +1,119 @@ +'BlueM.Wave +'Copyright (C) BlueM Dev Group +' +' +'This program is free software: you can redistribute it and/or modify +'it under the terms of the GNU Lesser General Public License as published by +'the Free Software Foundation, either version 3 of the License, or +'(at your option) any later version. +' +'This program is distributed in the hope that it will be useful, +'but WITHOUT ANY WARRANTY; without even the implied warranty of +'MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +'GNU Lesser General Public License for more details. +' +'You should have received a copy of the GNU Lesser General Public License +'along with this program. If not, see . +' +Imports System.Text +Imports Microsoft.VisualStudio.TestTools.UnitTesting + +''' +''' Tests for reading and writing WVP files +''' + Public Class TestWVP + + ''' + ''' Tests parsing WVP files + ''' + Public Sub TestWVP_Parse() + + Dim filesWVP As New List(Of String) From { + IO.Path.Combine(TestData.getTestDataDir(), "WVP", "test_all_filetypes.wvp"), + IO.Path.Combine(TestData.getTestDataDir(), "WVP", "test_displayoptions.wvp") + } + + For Each file As String In filesWVP + Dim wvp As New Fileformats.WVP(file) + Next + + End Sub + + ''' + ''' Tests reading WVP series options + ''' + Public Sub TestWVP_ReadOptions() + + Dim fileWVP As String = IO.Path.Combine(TestData.getTestDataDir(), "WVP", "test_displayoptions.wvp") + + Dim wvp As New Fileformats.WVP(fileWVP) + Dim tsList As List(Of TimeSeries) = wvp.Process() + + Assert.AreEqual("AA _1AB", tsList(0).Title) + Assert.AreEqual("AB _1AB custom title", tsList(1).Title) + Assert.AreEqual("AC _1AB custom title", tsList(2).Title) + With tsList(3) + Assert.AreEqual("AD _1AB ,= custom title", .Title) + Assert.AreEqual("m³/s", .Unit) + Assert.AreEqual(TimeSeries.InterpretationEnum.BlockLeft, .Interpretation) + Assert.AreEqual("Red", .DisplayOptions.Color.Name) + Assert.AreEqual(4, .DisplayOptions.LineWidth) + Assert.AreEqual(Drawing.Drawing2D.DashStyle.Dash, .DisplayOptions.LineStyle) + Assert.AreEqual(True, .DisplayOptions.ShowPoints) + End With + + End Sub + + ''' + ''' Tests writing WVP series options + ''' + Public Sub TestWVP_WriteOptions() + + 'read time series using a WVP fileOut + Dim fileIn As String = IO.Path.Combine(TestData.getTestDataDir(), "WVP", "test_displayoptions.wvp") + Dim wvp As New Fileformats.WVP(fileIn) + Dim tsList As List(Of TimeSeries) = wvp.Process() + + 'write the time series to a WVP fileOut + Dim fileOut As String = IO.Path.Combine(TestData.getTestDataDir(), "WVP", "test_displayoptions_export.wvp") + Call Fileformats.WVP.Write_File(tsList, fileOut, + saveRelativePaths:=True, + saveTitle:=True, + saveUnit:=True, + saveInterpretation:=True, + saveColor:=True, + saveLineStyle:=True, + saveLineWidth:=True, + savePointsVisibility:=True + ) + + 'check the fileOut contents of the written WVP fileOut + 'NOTE: because the series were never loaded in the chart, colors that were not set in the input wvp + 'will have the default value of Color.Empty (0) + Dim fileContents As String = New IO.StreamReader(fileOut, Text.Encoding.UTF8).ReadToEnd() + Dim lines As String() = fileContents.Split({vbCrLf}, StringSplitOptions.None) + + Assert.IsTrue(lines.Count = 7) + + Dim iLine As Integer = 0 + For Each line As String In lines + iLine += 1 + Select Case iLine + Case 1 + Assert.IsTrue(line.StartsWith("#")) + Case 2 + Assert.AreEqual("file=..\WEL\DEMONA_PSI.wel", line) + Case 3 + Assert.AreEqual(" series=AA _1AB: unit=""m3/s"", interpretation=BlockRight, color=0, linewidth=2, linestyle=Solid, showpoints=False", line) + Case 4 + Assert.AreEqual(" series=AB _1AB: title=""AB _1AB custom title"", unit=""m3/s"", interpretation=BlockRight, color=0, linewidth=2, linestyle=Solid, showpoints=False", line) + Case 5 + Assert.AreEqual(" series=AC _1AB: title=""AC _1AB custom title"", unit=""m3/s"", interpretation=BlockRight, color=0, linewidth=2, linestyle=Solid, showpoints=False", line) + Case 6 + Assert.AreEqual(" series=AD _1AB: title=""AD _1AB ,= custom title"", unit=""m³/s"", interpretation=BlockLeft, color=Red, linewidth=4, linestyle=Dash, showpoints=True", line) + End Select + Next + + End Sub + +End Class \ No newline at end of file diff --git a/tests/Wave.Tests.vbproj b/tests/Wave.Tests.vbproj index ea2a7d24..6a8baaff 100644 --- a/tests/Wave.Tests.vbproj +++ b/tests/Wave.Tests.vbproj @@ -108,6 +108,7 @@ Settings.settings True +