502 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			502 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
| using System;
 | |
| using System.Collections.Generic;
 | |
| using System.Drawing;
 | |
| using System.Linq;
 | |
| using System.Windows.Forms;
 | |
| 
 | |
| namespace CustomChartControl
 | |
| {
 | |
|     public class TimeVoltageChart : Control
 | |
|     {
 | |
|         private List<PointF> dataPoints;
 | |
|         private float zoomFactorX = 1.0f; // X축 줌 팩터
 | |
|         private float zoomFactorY = 1.0f; // Y축 줌 팩터
 | |
|         private PointF panOffset = PointF.Empty;
 | |
|         private Point lastMousePosition;
 | |
|         private bool isDragging = false;
 | |
|         private DateTime[] _timeData;
 | |
|         private float[] _voltageData;
 | |
|         private bool _showDataPoints = false;
 | |
| 
 | |
|         // 기존 필드 추가
 | |
|         private bool isSelecting = false; // 영역 선택 중 여부
 | |
|         private Point selectionStart; // 영역 선택 시작점
 | |
|         private Rectangle selectionRectangle; // 선택 영역
 | |
| 
 | |
|         public TimeVoltageChart()
 | |
|         {
 | |
|             this.DoubleBuffered = true;
 | |
|             this.dataPoints = new List<PointF>();
 | |
|             this.MouseWheel += TimeVoltageChart_MouseWheel;
 | |
|             this.MouseDown += TimeVoltageChart_MouseDown;
 | |
|             this.MouseMove += TimeVoltageChart_MouseMove;
 | |
|             this.MouseUp += TimeVoltageChart_MouseUp;
 | |
|         }
 | |
| 
 | |
|         public DateTime[] TimeData
 | |
|         {
 | |
|             get => _timeData;
 | |
|             set
 | |
|             {
 | |
|                 _timeData = value;
 | |
|                 UpdateDataPoints();
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         public float[] VoltageData
 | |
|         {
 | |
|             get => _voltageData;
 | |
|             set
 | |
|             {
 | |
|                 _voltageData = value;
 | |
|                 UpdateDataPoints();
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         public bool ShowDataPoints
 | |
|         {
 | |
|             get => _showDataPoints;
 | |
|             set
 | |
|             {
 | |
|                 _showDataPoints = value;
 | |
|                 Invalidate(); // 속성 변경 시 차트 다시 그리기
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         public void AdjustScaleX()
 | |
|         {
 | |
|             if (_timeData != null && _timeData.Length > 0)
 | |
|             {
 | |
|                 // X축 자동 스케일링
 | |
|                 float totalSeconds = (float)(_timeData[_timeData.Length - 1] - _timeData[0]).TotalSeconds;
 | |
|                 zoomFactorX = Width / totalSeconds;
 | |
|                 panOffset.X = 0; // 초기화
 | |
|                 Invalidate(); // 변경 후 다시 그리기
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         public void AdjustScaleY()
 | |
|         {
 | |
|             if (_voltageData != null && _voltageData.Length > 0)
 | |
|             {
 | |
|                 // Y축 자동 스케일링
 | |
|                 float minY = float.MaxValue;
 | |
|                 float maxY = float.MinValue;
 | |
|                 foreach (var voltage in _voltageData)
 | |
|                 {
 | |
|                     if (voltage < minY) minY = voltage;
 | |
|                     if (voltage > maxY) maxY = voltage;
 | |
|                 }
 | |
| 
 | |
|                 float range = maxY - minY;
 | |
|                 if (range == 0) range = 1;
 | |
| 
 | |
|                 zoomFactorY = Height / range;
 | |
|                 panOffset.Y = -minY * zoomFactorY; // 초기화
 | |
|                 Invalidate(); // 변경 후 다시 그리기
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         private void UpdateDataPoints()
 | |
|         {
 | |
|             dataPoints.Clear();
 | |
|             if (_timeData != null && _voltageData != null && _timeData.Length == _voltageData.Length)
 | |
|             {
 | |
|                 DateTime startTime = _timeData[0];
 | |
|                 for (int i = 0; i < _timeData.Length; i++)
 | |
|                 {
 | |
|                     float timeOffset = (float)(_timeData[i] - startTime).TotalSeconds;
 | |
|                     dataPoints.Add(new PointF(timeOffset, _voltageData[i]));
 | |
|                 }
 | |
|                 Invalidate(); // 화면을 갱신하여 차트를 다시 그리도록 함
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         protected override void OnPaint(PaintEventArgs e)
 | |
|         {
 | |
|             base.OnPaint(e);
 | |
|             DrawGrid(e.Graphics);
 | |
|             DrawData(e.Graphics);
 | |
|             DrawBorder(e.Graphics);
 | |
|             DrawZoomLevel(e.Graphics);
 | |
| 
 | |
|             if (isSelecting && selectionRectangle.Width > 0 && selectionRectangle.Height > 0)
 | |
|             {
 | |
|                 using (var selectionPen = new Pen(Color.Gray, 1))
 | |
|                 {
 | |
|                     selectionPen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dash;
 | |
|                     e.Graphics.DrawRectangle(selectionPen, selectionRectangle);
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         private void DrawZoomLevel(Graphics g)
 | |
|         {
 | |
|             string zoomLevelText = $"Zoom Level - X: {zoomFactorX:F2}, Y: {zoomFactorY:F2}";
 | |
|             Font zoomLevelFont = new Font("Arial", 10, FontStyle.Bold);
 | |
|             Brush zoomLevelBrush = new SolidBrush(Color.Black);
 | |
|             SizeF textSize = g.MeasureString(zoomLevelText, zoomLevelFont);
 | |
| 
 | |
|             float x = Width - textSize.Width - 60; // 오른쪽 여백을 줄이기
 | |
|             float y = 10; // 상단 여백 10
 | |
| 
 | |
|             g.DrawString(zoomLevelText, zoomLevelFont, zoomLevelBrush, x, y);
 | |
|         }
 | |
| 
 | |
|         private void DrawBorder(Graphics g)
 | |
|         {
 | |
|             int leftMargin = 50; // 왼쪽 Y축 라벨을 위한 공간
 | |
|             int rightMargin = 50; // 오른쪽 여백
 | |
|             int bottomMargin = 30; // X축 라벨을 위한 공간
 | |
| 
 | |
|             Pen borderPen = new Pen(Color.Black, 1); // 테두리 색상 및 두께
 | |
|             Rectangle borderRect = new Rectangle(leftMargin, 0, Width - leftMargin - rightMargin, Height - bottomMargin - 1); // 테두리 영역
 | |
|             g.DrawRectangle(borderPen, borderRect);
 | |
|         }
 | |
|         private void DrawGrid(Graphics g)
 | |
|         {
 | |
|             int gridSpacingX = 60; // X축 그리드 간격
 | |
|             int gridSpacingY = 20; // Y축 그리드 간격
 | |
|             int leftMargin = 50; // 왼쪽 Y축 라벨을 위한 공간
 | |
|             int rightMargin = 50; // 오른쪽 여백
 | |
|             int bottomMargin = 30; // X축 라벨을 위한 공간
 | |
| 
 | |
|             Pen gridPen = new Pen(Color.LightGray);
 | |
|             Brush labelBrush = new SolidBrush(Color.Black);
 | |
|             Font labelFont = new Font("Arial", 8);
 | |
| 
 | |
|             if (_timeData != null && _timeData.Length > 0)
 | |
|             {
 | |
|                 DateTime minTime = _timeData[0];
 | |
|                 DateTime maxTime = _timeData[_timeData.Length - 1]; // 마지막 요소
 | |
| 
 | |
|                 double totalSeconds = (maxTime - minTime).TotalSeconds;
 | |
| 
 | |
|                 float visibleStartTime = (0 - panOffset.X) / zoomFactorX;
 | |
|                 float visibleEndTime = (Width - leftMargin - rightMargin - panOffset.X) / zoomFactorX;
 | |
| 
 | |
|                 for (int x = leftMargin; x < Width - rightMargin; x += gridSpacingX)
 | |
|                 {
 | |
|                     float timeOffset = (x - leftMargin - panOffset.X) / zoomFactorX;
 | |
|                     if (timeOffset < visibleStartTime || timeOffset > visibleEndTime)
 | |
|                         continue;
 | |
| 
 | |
|                     DateTime time = minTime.AddSeconds(timeOffset);
 | |
|                     g.DrawLine(gridPen, x, 0, x, Height - bottomMargin); // 테두리 위에 맞추기
 | |
| 
 | |
|                     using (StringFormat sf = new StringFormat())
 | |
|                     {
 | |
|                         sf.LineAlignment = StringAlignment.Near;
 | |
|                         sf.Alignment = StringAlignment.Center;
 | |
|                         g.DrawString(time.ToString("yy-MM-dd\nHH:mm:ss"), labelFont, labelBrush, new PointF(x, Height - bottomMargin), sf);
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             if (_voltageData != null && _voltageData.Length > 0)
 | |
|             {
 | |
|                 float minY = float.MaxValue;
 | |
|                 float maxY = float.MinValue;
 | |
|                 foreach (var voltage in _voltageData)
 | |
|                 {
 | |
|                     if (voltage < minY) minY = voltage;
 | |
|                     if (voltage > maxY) maxY = voltage;
 | |
|                 }
 | |
| 
 | |
|                 float range = maxY - minY;
 | |
|                 if (range == 0) range = 1;
 | |
| 
 | |
|                 using (StringFormat sfY = new StringFormat())
 | |
|                 {
 | |
|                     sfY.LineAlignment = StringAlignment.Center;
 | |
|                     sfY.Alignment = StringAlignment.Far; // Y축 라벨을 오른쪽 정렬
 | |
|                     for (int y = 0; y < Height - bottomMargin; y += gridSpacingY) // 테두리 위에 맞추기
 | |
|                     {
 | |
|                         float voltageValue = minY + (Height - y - panOffset.Y) / zoomFactorY * range / (Height - bottomMargin);
 | |
| 
 | |
|                         // 모든 Y축 눈금 라벨을 표시
 | |
|                         g.DrawLine(gridPen, leftMargin, y, Width - rightMargin, y); // 패딩 추가
 | |
|                         g.DrawString(voltageValue.ToString("F2"), labelFont, labelBrush, new PointF(leftMargin - 10, y), sfY);
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         private void DrawData(Graphics g)
 | |
|         {
 | |
|             if (dataPoints.Count < 2) return;
 | |
| 
 | |
|             int leftMargin = 50;
 | |
|             int rightMargin = 50;
 | |
|             int bottomMargin = 30;
 | |
|             int minPixelDistance = 3; // 최소 픽셀 거리 설정
 | |
| 
 | |
|             Pen dataPen = new Pen(Color.Blue);
 | |
|             Brush pointBrush = new SolidBrush(Color.Red); // 데이터 포인트의 원 색상
 | |
| 
 | |
|             float visibleStartTime = (0 - panOffset.X) / zoomFactorX;
 | |
|             float visibleEndTime = (Width - leftMargin - rightMargin - panOffset.X) / zoomFactorX;
 | |
| 
 | |
|             PointF prevPoint = TransformPoint(dataPoints[0]);
 | |
|             PointF lastDrawnPoint = prevPoint;
 | |
|             bool skipNextPoint = false;
 | |
| 
 | |
|             // 포인트 간 평균 간격 계산
 | |
|             float totalWidth = Width - leftMargin - rightMargin;
 | |
|             float avgPixelDistance = totalWidth / dataPoints.Count;
 | |
|             int pointRadius = Math.Max(1, (int)(avgPixelDistance / 4)); // 밀집도에 따른 포인트 반지름 조정
 | |
| 
 | |
|             for (int i = 1; i < dataPoints.Count; i++)
 | |
|             {
 | |
|                 float timeOffset = dataPoints[i].X;
 | |
|                 if (timeOffset < visibleStartTime || timeOffset > visibleEndTime)
 | |
|                 {
 | |
|                     prevPoint = TransformPoint(dataPoints[i]);
 | |
|                     continue;
 | |
|                 }
 | |
| 
 | |
|                 PointF currPoint = TransformPoint(dataPoints[i]);
 | |
|                 if (currPoint.X >= leftMargin && currPoint.X <= Width - rightMargin)
 | |
|                 {
 | |
|                     PointF clippedPrev = ClipToChartArea(prevPoint, leftMargin, Width - rightMargin, bottomMargin, Height);
 | |
|                     PointF clippedCurr = ClipToChartArea(currPoint, leftMargin, Width - rightMargin, bottomMargin, Height);
 | |
| 
 | |
|                     // 선 연결을 유지하되, 포인트를 그리지 않을 때만 스킵
 | |
|                     if (skipNextPoint || Math.Abs(clippedCurr.X - lastDrawnPoint.X) >= minPixelDistance)
 | |
|                     {
 | |
|                         g.DrawLine(dataPen, clippedPrev, clippedCurr);
 | |
|                         lastDrawnPoint = clippedCurr;
 | |
|                         skipNextPoint = false;
 | |
| 
 | |
|                         if (clippedCurr.X > leftMargin && clippedCurr.X < Width - rightMargin && clippedCurr.Y > 0 && clippedCurr.Y < Height - bottomMargin)
 | |
|                         {
 | |
|                             g.FillEllipse(pointBrush, clippedCurr.X - pointRadius, clippedCurr.Y - pointRadius, pointRadius * 2, pointRadius * 2);
 | |
|                         }
 | |
|                     }
 | |
|                     else
 | |
|                     {
 | |
|                         skipNextPoint = true;
 | |
|                     }
 | |
|                 }
 | |
|                 prevPoint = currPoint;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         private PointF ClipToChartArea(PointF point, int leftMargin, int rightLimit, int bottomMargin, int topLimit)
 | |
|         {
 | |
|             // X축 클리핑
 | |
|             if (point.X < leftMargin) point.X = leftMargin;
 | |
|             if (point.X > rightLimit) point.X = rightLimit;
 | |
| 
 | |
|             // Y축 클리핑
 | |
|             if (point.Y < 0) point.Y = 0;
 | |
|             if (point.Y > topLimit - bottomMargin) point.Y = topLimit - bottomMargin;
 | |
| 
 | |
|             return point;
 | |
|         }
 | |
|         private PointF TransformPoint(PointF point)
 | |
|         {
 | |
|             int leftMargin = 50;
 | |
|             int bottomMargin = 30;
 | |
|             return new PointF(
 | |
|                 (point.X * zoomFactorX + panOffset.X) + leftMargin,
 | |
|                 Height - ((point.Y * zoomFactorY - panOffset.Y) + bottomMargin));
 | |
|         }
 | |
| 
 | |
| 
 | |
| 
 | |
|         private void TimeVoltageChart_MouseWheel(object sender, MouseEventArgs e)
 | |
|         {
 | |
|             float zoomChange;
 | |
| 
 | |
|             if (ModifierKeys.HasFlag(Keys.Control))
 | |
|             {
 | |
|                 // Y축 줌 레벨 조정
 | |
|                 zoomChange = e.Delta > 0 ? 1.1f : 0.9f;
 | |
|                 float newZoomFactorY = zoomFactorY * zoomChange;
 | |
|                 if (CanZoomY(newZoomFactorY))
 | |
|                 {
 | |
|                     AdjustPanOffsetY(newZoomFactorY, e.Delta > 0);
 | |
|                     zoomFactorY = newZoomFactorY;
 | |
|                 }
 | |
|             }
 | |
|             else
 | |
|             {
 | |
|                 // X축 줌 레벨 조정
 | |
|                 zoomChange = e.Delta > 0 ? 1.1f : 0.9f;
 | |
|                 float newZoomFactorX = zoomFactorX * zoomChange;
 | |
|                 if (CanZoomX(newZoomFactorX))
 | |
|                 {
 | |
|                     AdjustPanOffsetX(newZoomFactorX, e.Delta > 0);
 | |
|                     zoomFactorX = newZoomFactorX;
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             Invalidate();
 | |
|         }
 | |
| 
 | |
|         private void AdjustPanOffsetX(float newZoomFactorX, bool zoomingIn)
 | |
|         {
 | |
|             float totalDataTime = (float)(_timeData[_timeData.Length - 1] - _timeData[0]).TotalSeconds;
 | |
|             float currentVisibleStart = -panOffset.X / zoomFactorX;
 | |
|             float currentVisibleEnd = (Width - panOffset.X) / zoomFactorX;
 | |
|             float newVisibleStart, newVisibleEnd;
 | |
| 
 | |
|             if (zoomingIn)
 | |
|             {
 | |
|                 newVisibleStart = currentVisibleStart + (currentVisibleEnd - currentVisibleStart) * 0.1f;
 | |
|                 newVisibleEnd = currentVisibleEnd - (currentVisibleEnd - currentVisibleStart) * 0.1f;
 | |
|             }
 | |
|             else
 | |
|             {
 | |
|                 newVisibleStart = currentVisibleStart - (currentVisibleEnd - currentVisibleStart) * 0.1f;
 | |
|                 newVisibleEnd = currentVisibleEnd + (currentVisibleEnd - currentVisibleStart) * 0.1f;
 | |
|             }
 | |
| 
 | |
|             if (newVisibleStart < 0)
 | |
|             {
 | |
|                 panOffset.X = 0;
 | |
|             }
 | |
|             else if (newVisibleEnd > totalDataTime)
 | |
|             {
 | |
|                 panOffset.X = -(totalDataTime * newZoomFactorX - Width);
 | |
|             }
 | |
|             else
 | |
|             {
 | |
|                 panOffset.X = -newVisibleStart * newZoomFactorX;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         private void AdjustPanOffsetY(float newZoomFactorY, bool zoomingIn)
 | |
|         {
 | |
|             float minY = float.MaxValue;
 | |
|             float maxY = float.MinValue;
 | |
|             foreach (var voltage in _voltageData)
 | |
|             {
 | |
|                 if (voltage < minY) minY = voltage;
 | |
|                 if (voltage > maxY) maxY = voltage;
 | |
|             }
 | |
| 
 | |
|             float dataRange = maxY - minY;
 | |
|             float visibleRange = (Height - 70) / newZoomFactorY; // 상하 여백 고려
 | |
|             float visibleStartY = -panOffset.Y / zoomFactorY;
 | |
|             float visibleEndY = visibleStartY + visibleRange;
 | |
| 
 | |
|             // Ensure the visible range is within the data range
 | |
|             if (visibleStartY < minY)
 | |
|             {
 | |
|                 visibleStartY = minY;
 | |
|                 visibleEndY = visibleStartY + visibleRange;
 | |
|             }
 | |
|             if (visibleEndY > maxY)
 | |
|             {
 | |
|                 visibleEndY = maxY;
 | |
|                 visibleStartY = visibleEndY - visibleRange;
 | |
|             }
 | |
| 
 | |
|             // Adjust panOffset.Y to ensure the data is visible within the screen bounds
 | |
|             panOffset.Y = -visibleStartY * newZoomFactorY;
 | |
|         }
 | |
| 
 | |
| 
 | |
|         private bool CanZoomX(float newZoomFactorX)
 | |
|         {
 | |
|             float totalDataTime = (float)(_timeData[_timeData.Length - 1] - _timeData[0]).TotalSeconds;
 | |
|             float visibleTime = (Width - 100) / newZoomFactorX; // 좌우 여백 고려
 | |
| 
 | |
|             float marginFactor = 0.05f; // 5% 여백
 | |
|             float minVisibleTime = 1.0f; // 최소 가시 시간
 | |
| 
 | |
|             return visibleTime < totalDataTime * (1 + marginFactor) && visibleTime > minVisibleTime;
 | |
|         }
 | |
| 
 | |
|         private bool CanZoomY(float newZoomFactorY)
 | |
|         {
 | |
|             float minY = float.MaxValue;
 | |
|             float maxY = float.MinValue;
 | |
|             foreach (var voltage in _voltageData)
 | |
|             {
 | |
|                 if (voltage < minY) minY = voltage;
 | |
|                 if (voltage > maxY) maxY = voltage;
 | |
|             }
 | |
| 
 | |
|             float dataRange = maxY - minY;
 | |
|             float visibleRange = (Height - 70) / newZoomFactorY; // 상하 여백 고려
 | |
| 
 | |
|             float marginFactor = 0.05f; // 5% 여백
 | |
|             float minVisibleRange = 0.1f; // 최소 가시 범위
 | |
| 
 | |
|             return visibleRange < dataRange * (1 + marginFactor) && visibleRange > minVisibleRange;
 | |
|         }
 | |
| 
 | |
|         private void TimeVoltageChart_MouseDown(object sender, MouseEventArgs e)
 | |
|         {
 | |
|             if (e.Button == MouseButtons.Left)
 | |
|             {
 | |
|                 isDragging = true;
 | |
|                 lastMousePosition = e.Location;
 | |
|             }
 | |
|             else if (e.Button == MouseButtons.Right)
 | |
|             {
 | |
|                 isSelecting = true;
 | |
|                 selectionStart = e.Location;
 | |
|                 selectionRectangle = new Rectangle(e.Location, Size.Empty);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         private void TimeVoltageChart_MouseMove(object sender, MouseEventArgs e)
 | |
|         {
 | |
|             if (isDragging)
 | |
|             {
 | |
|                 panOffset.X += e.X - lastMousePosition.X;
 | |
|                 panOffset.Y += e.Y - lastMousePosition.Y; // Y축 이동 방향 수정
 | |
|                 lastMousePosition = e.Location;
 | |
|                 Invalidate();
 | |
|             }
 | |
|             else if (isSelecting)
 | |
|             {
 | |
|                 var endPoint = e.Location;
 | |
|                 selectionRectangle = new Rectangle(
 | |
|                     Math.Min(selectionStart.X, endPoint.X),
 | |
|                     Math.Min(selectionStart.Y, endPoint.Y),
 | |
|                     Math.Abs(selectionStart.X - endPoint.X),
 | |
|                     Math.Abs(selectionStart.Y - endPoint.Y));
 | |
|                 Invalidate();
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         private void TimeVoltageChart_MouseUp(object sender, MouseEventArgs e)
 | |
|         {
 | |
|             if (e.Button == MouseButtons.Left)
 | |
|             {
 | |
|                 isDragging = false;
 | |
|             }
 | |
|             else if (e.Button == MouseButtons.Right)
 | |
|             {
 | |
|                 isSelecting = false;
 | |
| 
 | |
|                 // 선택한 영역만큼 확대
 | |
|                 if (selectionRectangle.Width > 0 && selectionRectangle.Height > 0)
 | |
|                 {
 | |
|                     float newZoomFactorX = (Width - 100) / (float)selectionRectangle.Width; // 여백을 고려한 줌
 | |
|                     float newZoomFactorY = (Height - 30) / (float)selectionRectangle.Height; // 여백을 고려한 줌
 | |
| 
 | |
|                     // 확대할 영역의 좌표를 차트 좌표로 변환
 | |
|                     float selectedMinX = (selectionRectangle.Left - 50 - panOffset.X) / zoomFactorX;
 | |
|                     float selectedMaxX = (selectionRectangle.Right - 50 - panOffset.X) / zoomFactorX;
 | |
|                     float selectedMinY = (Height - selectionRectangle.Bottom - panOffset.Y) / zoomFactorY;
 | |
|                     float selectedMaxY = (Height - selectionRectangle.Top - panOffset.Y) / zoomFactorY;
 | |
| 
 | |
|                     zoomFactorX = newZoomFactorX;
 | |
|                     zoomFactorY = newZoomFactorY;
 | |
| 
 | |
|                     panOffset.X = -selectedMinX * zoomFactorX + 50;
 | |
|                     panOffset.Y = -selectedMinY * zoomFactorY;
 | |
| 
 | |
|                     Invalidate();
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| }
 | 
