// Copyright (C) Stichting Deltares and State of the Netherlands 2025. All rights reserved. // // This file is part of Riskeer. // // Riskeer 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 . // // All names, logos, and references to "Deltares" are registered trademarks of // Stichting Deltares and remain full property of Stichting Deltares at all times. // All rights reserved. using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Text; using System.Linq; using System.Windows.Forms; using Core.Common.Base; using Core.Common.Util.Drawing; using Core.Components.Chart.Data; using Core.Components.Chart.Forms; using Core.Components.OxyPlot.DataSeries.Chart; using Core.Components.OxyPlot.Forms.Properties; using OxyPlot; using OxyPlot.Series; namespace Core.Components.OxyPlot.Forms { /// /// This class describes a plot view with configured representation of axes. /// public partial class ChartControl : UserControl, IChartControl { private static readonly PrivateFontCollection privateFontCollection = new PrivateFontCollection(); private static readonly Font font = FontHelper.CreateFont(Resources.Symbols, privateFontCollection); private readonly RecursiveObserver chartDataCollectionObserver; private readonly List drawnChartDataList = new List(); private LinearPlotView plotView; private DynamicPlotController plotController; private ChartDataCollection data; /// /// Creates a new instance of . /// public ChartControl() { InitializeComponent(); SetFonts(); InitializePlotView(); panToolStripButton.PerformClick(); chartDataCollectionObserver = new RecursiveObserver(HandleChartDataCollectionChange, cdc => cdc.Collection); } public ChartDataCollection Data { get { return data; } set { if (data != null) { ClearChartData(); } data = value; chartDataCollectionObserver.Observable = data; if (data != null) { DrawInitialChartData(); } plotView.InvalidatePlot(true); } } public string ChartTitle { get { return plotView.ModelTitle; } set { plotView.ModelTitle = value; } } public string BottomAxisTitle { get { return plotView.BottomAxisTitle; } set { plotView.BottomAxisTitle = value; } } public string LeftAxisTitle { get { return plotView.LeftAxisTitle; } set { plotView.LeftAxisTitle = value; } } public void ZoomToVisibleSeries(ChartData chartData) { Extent extent = CreateEnvelopeForVisibleSeries(chartData); if (!extent.IsNaN) { Extent extentWithPadding = extent.AddPadding(0.01); plotView.SetExtent(extentWithPadding); plotView.Refresh(); } } protected override void Dispose(bool disposing) { plotView.Dispose(); chartDataCollectionObserver.Dispose(); if (disposing) { components?.Dispose(); } base.Dispose(disposing); } private void SetFonts() { panToolStripButton.Font = font; zoomToRectangleToolStripButton.Font = font; zoomToVisibleSeriesToolStripButton.Font = font; } private void InitializePlotView() { plotController = new DynamicPlotController(); plotView = new LinearPlotView { BackColor = Color.White, Model = { IsLegendVisible = false, PlotMargins = new OxyThickness(double.NaN, double.NaN, 25, double.NaN) }, Controller = plotController, Dock = DockStyle.Fill }; tableLayoutPanel.Controls.Add(plotView, 0, 0); } /// /// Defines the area taken up by the visible chart data based on the provided chart data. /// /// The data to determine the visible extent for. /// The area definition. /// Thrown when is /// not part of the drawn chart data. private Extent CreateEnvelopeForVisibleSeries(ChartData chartData) { if (chartData is ChartDataCollection collection) { return CreateEnvelopeForVisibleSeries(collection); } DrawnChartData drawnChartData = drawnChartDataList.FirstOrDefault(dmd => dmd.ChartData.Equals(chartData)); if (drawnChartData == null) { throw new ArgumentException($@"Can only zoom to {nameof(ChartData)} that is part of this {nameof(ChartControl)}s drawn {nameof(chartData)}.", nameof(chartData)); } var extent = new Extent(); ChartData chartDataDrawn = drawnChartData.ChartData; if (chartDataDrawn.IsVisible && chartDataDrawn.HasData) { extent.ExpandToInclude(CreateExtentFor(drawnChartData.ChartDataSeries as XYAxisSeries)); } return extent; } /// /// Defines the area taken up by the visible chart data based on the provided chart data. /// /// The data to determine the visible extent for. /// The area definition. /// Thrown when or /// any of its children is not part of the drawn chart data. private Extent CreateEnvelopeForVisibleSeries(ChartDataCollection chartDataCollection) { var envelope = new Extent(); foreach (ChartData childChartData in chartDataCollection.Collection) { envelope.ExpandToInclude(CreateEnvelopeForVisibleSeries(childChartData)); } return envelope; } private static Extent CreateExtentFor(XYAxisSeries chartData) { return new Extent ( chartData.MinX, chartData.MaxX, chartData.MinY, chartData.MaxY ); } private void HandleChartDataCollectionChange() { List chartDataThatShouldBeDrawn = Data.GetChartDataRecursively().ToList(); Dictionary drawnChartDataLookup = drawnChartDataList.ToDictionary(dcd => dcd.ChartData, dcd => dcd); DrawMissingChartDataOnCollectionChange(chartDataThatShouldBeDrawn, drawnChartDataLookup); RemoveRedundantChartDataOnCollectionChange(chartDataThatShouldBeDrawn, drawnChartDataLookup); drawnChartDataLookup = drawnChartDataList.ToDictionary(dcd => dcd.ChartData, dcd => dcd); ReorderChartDataOnCollectionChange(chartDataThatShouldBeDrawn, drawnChartDataLookup); plotView.InvalidatePlot(true); } private void DrawMissingChartDataOnCollectionChange(IEnumerable chartDataThatShouldBeDrawn, IDictionary drawnChartDataLookup) { foreach (ChartData chartDataToDraw in chartDataThatShouldBeDrawn.Where(chartDataToDraw => !drawnChartDataLookup.ContainsKey(chartDataToDraw))) { DrawChartData(chartDataToDraw); } } private void RemoveRedundantChartDataOnCollectionChange(IEnumerable chartDataThatShouldBeDrawn, IDictionary drawnChartDataLookup) { foreach (ChartData chartData in drawnChartDataLookup.Keys.Except(chartDataThatShouldBeDrawn)) { RemoveChartData(drawnChartDataLookup[chartData]); } } private void ReorderChartDataOnCollectionChange(IEnumerable chartDataThatShouldBeDrawn, IDictionary drawnChartDataLookup) { plotView.Model.Series.Clear(); foreach (ChartData chartData in chartDataThatShouldBeDrawn) { plotView.Model.Series.Add((Series) drawnChartDataLookup[chartData].ChartDataSeries); } } private void RemoveChartData(DrawnChartData drawnChartDataToRemove) { drawnChartDataToRemove.Observer.Dispose(); drawnChartDataList.Remove(drawnChartDataToRemove); plotView.Model.Series.Remove((Series) drawnChartDataToRemove.ChartDataSeries); } private void DrawInitialChartData() { foreach (ChartData chartData in Data.GetChartDataRecursively()) { DrawChartData(chartData); } } private void DrawChartData(ChartData chartData) { IChartDataSeries chartDataSeries = ChartDataSeriesFactory.Create(chartData); var drawnChartData = new DrawnChartData { ChartData = chartData, ChartDataSeries = chartDataSeries }; drawnChartData.Observer = new Observer(() => { drawnChartData.ChartDataSeries.Update(); plotView.InvalidatePlot(true); }) { Observable = chartData }; drawnChartDataList.Add(drawnChartData); plotView.Model.Series.Add((Series) chartDataSeries); } private void ClearChartData() { foreach (DrawnChartData drawnChartData in drawnChartDataList) { drawnChartData.Observer.Dispose(); } drawnChartDataList.Clear(); plotView.Model.Series.Clear(); } private void PanToolStripButtonClick(object sender, EventArgs e) { if (panToolStripButton.Checked) { return; } plotController.TogglePanning(); panToolStripButton.Checked = true; zoomToRectangleToolStripButton.Checked = false; } private void ZoomToRectangleToolStripButtonClick(object sender, EventArgs e) { if (zoomToRectangleToolStripButton.Checked) { return; } plotController.ToggleRectangleZooming(); panToolStripButton.Checked = false; zoomToRectangleToolStripButton.Checked = true; } private void ZoomToVisibleSeriesToolStripButtonClick(object sender, EventArgs e) { ZoomToVisibleSeries(Data); } /// /// Lookup class for administration related to drawn chart data series. /// private class DrawnChartData { /// /// The chart data which the drawn is based upon. /// public ChartData ChartData { get; set; } /// /// The drawn chart data series. /// public IChartDataSeries ChartDataSeries { get; set; } /// /// The observer attached to and responsible for updating . /// public Observer Observer { get; set; } } } }