public void GetBarWidth_OrdinalAxisWeeklyValues_3Pixels() { GraphPane myPane = new GraphPane(); myPane.Rect = new RectangleF(0, 0, 640f, 480f); myPane.XAxis.Type = AxisType.DateAsOrdinal; StockPointList spl = CreateStockPointList(60 * 24 * 7); OHLCBarItem myCurve = myPane.AddOHLCBar("trades", spl, Color.Black); AxisChangeAndDraw(myPane); Assert.That(myCurve.Bar.GetBarWidth(myPane, myPane.XAxis, 1.0f), Is.EqualTo(3f)); }
/// <summary> /// Add a candlestick graph (<see cref="OHLCBarItem"/> object) to the plot with /// the given data points (<see cref="IPointList"/>) and properties. /// </summary> /// <remarks> /// This is simplified way to add curves without knowledge of the /// <see cref="CurveList"/> class. An alternative is to use /// the <see cref="ZedGraph.CurveList" /> Add() method. /// Note that the <see cref="IPointList" /> /// should contain <see cref="StockPt" /> objects instead of <see cref="PointPair" /> /// objects in order to contain all the data values required for this curve type. /// </remarks> /// <param name="label">The text label (string) for the curve that will be /// used as a <see cref="Legend"/> entry.</param> /// <param name="points">A <see cref="IPointList"/> of double precision value pairs that define /// the X and Y values for this curve</param> /// <param name="color">The color to used for the curve line, /// symbols, etc.</param> /// <returns>A <see cref="CurveItem"/> class for the newly created curve. /// This can then be used to access all of the curve properties that /// are not defined as arguments to the /// <see cref="AddOHLCBar(string,IPointList,Color)"/> method.</returns> public OHLCBarItem AddOHLCBar( string label, IPointList points, Color color ) { OHLCBarItem curve = new OHLCBarItem( label, points, color ); _curveList.Add( curve ); return curve; }
/// <summary> /// Draw all the <see cref="OHLCBar"/>'s to the specified <see cref="Graphics"/> /// device as a candlestick at each defined point. /// </summary> /// <param name="g"> /// A graphic device object to be drawn into. This is normally e.Graphics from the /// PaintEventArgs argument to the Paint() method. /// </param> /// <param name="pane"> /// A reference to the <see cref="GraphPane"/> object that is the parent or /// owner of this object. /// </param> /// <param name="curve">A <see cref="OHLCBarItem"/> object representing the /// <see cref="OHLCBar"/>'s to be drawn.</param> /// <param name="baseAxis">The <see cref="Axis"/> class instance that defines the base (independent) /// axis for the <see cref="OHLCBar"/></param> /// <param name="valueAxis">The <see cref="Axis"/> class instance that defines the value (dependent) /// axis for the <see cref="OHLCBar"/></param> /// <param name="scaleFactor"> /// The scaling factor to be used for rendering objects. This is calculated and /// passed down by the parent <see cref="GraphPane"/> object using the /// <see cref="PaneBase.CalcScaleFactor"/> method, and is used to proportionally adjust /// font sizes, etc. according to the actual size of the graph. /// </param> public void Draw( Graphics g, GraphPane pane, OHLCBarItem curve, Axis baseAxis, Axis valueAxis, float scaleFactor ) { //ValueHandler valueHandler = new ValueHandler( pane, false ); float pixBase, pixHigh, pixLow, pixOpen, pixClose; if ( curve.Points != null ) { //float halfSize = _size * scaleFactor; float halfSize = GetBarWidth( pane, baseAxis, scaleFactor ); using ( Pen pen = !curve.IsSelected ? new Pen( _color, _width ) : new Pen( Selection.Border.Color, Selection.Border.Width ) ) // using ( Pen pen = new Pen( _color, _penWidth ) ) { // Loop over each defined point for ( int i = 0; i < curve.Points.Count; i++ ) { PointPair pt = curve.Points[i]; double date = pt.X; double high = pt.Y; double low = pt.Z; double open = PointPair.Missing; double close = PointPair.Missing; if ( pt is StockPt ) { open = ( pt as StockPt ).Open; close = ( pt as StockPt ).Close; } // Any value set to double max is invalid and should be skipped // This is used for calculated values that are out of range, divide // by zero, etc. // Also, any value <= zero on a log scale is invalid if ( !curve.Points[i].IsInvalid3D && ( date > 0 || !baseAxis._scale.IsLog ) && ( ( high > 0 && low > 0 ) || !valueAxis._scale.IsLog ) ) { pixBase = (int)( baseAxis.Scale.Transform( curve.IsOverrideOrdinal, i, date ) + 0.5 ); //pixBase = baseAxis.Scale.Transform( curve.IsOverrideOrdinal, i, date ); pixHigh = valueAxis.Scale.Transform( curve.IsOverrideOrdinal, i, high ); pixLow = valueAxis.Scale.Transform( curve.IsOverrideOrdinal, i, low ); if ( PointPair.IsValueInvalid( open ) ) pixOpen = Single.MaxValue; else pixOpen = valueAxis.Scale.Transform( curve.IsOverrideOrdinal, i, open ); if ( PointPair.IsValueInvalid( close ) ) pixClose = Single.MaxValue; else pixClose = valueAxis.Scale.Transform( curve.IsOverrideOrdinal, i, close ); if ( !curve.IsSelected && this._gradientFill.IsGradientValueType ) { using ( Pen tPen = GetPen( pane, scaleFactor, pt ) ) Draw( g, pane, baseAxis is XAxis || baseAxis is X2Axis, pixBase, pixHigh, pixLow, pixOpen, pixClose, halfSize, tPen ); } else Draw( g, pane, baseAxis is XAxis || baseAxis is X2Axis, pixBase, pixHigh, pixLow, pixOpen, pixClose, halfSize, pen ); } } } } }
/// <summary> /// The Copy Constructor /// </summary> /// <param name="rhs">The <see cref="OHLCBarItem"/> object from which to copy</param> public OHLCBarItem(OHLCBarItem rhs) : base(rhs) { Bar = rhs.Bar.Clone(); }
/// <summary> /// Draw all the <see cref="OHLCBar"/>'s to the specified <see cref="Graphics"/> /// device as a candlestick at each defined point. /// </summary> /// <param name="g"> /// A graphic device object to be drawn into. This is normally e.Graphics from the /// PaintEventArgs argument to the Paint() method. /// </param> /// <param name="pane"> /// A reference to the <see cref="GraphPane"/> object that is the parent or /// owner of this object. /// </param> /// <param name="curve">A <see cref="OHLCBarItem"/> object representing the /// <see cref="OHLCBar"/>'s to be drawn.</param> /// <param name="baseAxis">The <see cref="Axis"/> class instance that defines the base (independent) /// axis for the <see cref="OHLCBar"/></param> /// <param name="valueAxis">The <see cref="Axis"/> class instance that defines the value (dependent) /// axis for the <see cref="OHLCBar"/></param> /// <param name="scaleFactor"> /// The scaling factor to be used for rendering objects. This is calculated and /// passed down by the parent <see cref="GraphPane"/> object using the /// <see cref="PaneBase.CalcScaleFactor"/> method, and is used to proportionally adjust /// font sizes, etc. according to the actual size of the graph. /// </param> public void Draw(Graphics g, GraphPane pane, OHLCBarItem curve, Axis baseAxis, Axis valueAxis, float scaleFactor) { //ValueHandler valueHandler = new ValueHandler( pane, false ); float pixBase, pixHigh, pixLow, pixOpen, pixClose; if (curve.Points != null) { //float halfSize = _size * scaleFactor; float halfSize = GetBarWidth(pane, baseAxis, scaleFactor); using (Pen pen = !curve.IsSelected ? new Pen(_color, _width) : new Pen(Selection.Border.Color, Selection.Border.Width)) // using ( Pen pen = new Pen( _color, _penWidth ) ) { // Loop over each defined point for (int i = 0; i < curve.Points.Count; i++) { PointPair pt = curve.Points[i]; double date = pt.X; double high = pt.Y; double low = pt.Z; double open = PointPair.Missing; double close = PointPair.Missing; if (pt is StockPt) { open = (pt as StockPt).Open; close = (pt as StockPt).Close; } // Any value set to double max is invalid and should be skipped // This is used for calculated values that are out of range, divide // by zero, etc. // Also, any value <= zero on a log scale is invalid if (!curve.Points[i].IsInvalid3D && (date > 0 || !baseAxis._scale.IsLog) && ((high > 0 && low > 0) || !valueAxis._scale.IsLog)) { pixBase = (int)(baseAxis.Scale.Transform(curve.IsOverrideOrdinal, i, date) + 0.5); //pixBase = baseAxis.Scale.Transform( curve.IsOverrideOrdinal, i, date ); pixHigh = valueAxis.Scale.Transform(curve.IsOverrideOrdinal, i, high); pixLow = valueAxis.Scale.Transform(curve.IsOverrideOrdinal, i, low); if (PointPair.IsValueInvalid(open)) { pixOpen = Single.MaxValue; } else { pixOpen = valueAxis.Scale.Transform(curve.IsOverrideOrdinal, i, open); } if (PointPair.IsValueInvalid(close)) { pixClose = Single.MaxValue; } else { pixClose = valueAxis.Scale.Transform(curve.IsOverrideOrdinal, i, close); } if (!curve.IsSelected && this._gradientFill.IsGradientValueType) { using (Pen tPen = GetPen(pane, scaleFactor, pt)) Draw(g, pane, baseAxis is XAxis || baseAxis is X2Axis, pixBase, pixHigh, pixLow, pixOpen, pixClose, halfSize, tPen); } else { Draw(g, pane, baseAxis is XAxis || baseAxis is X2Axis, pixBase, pixHigh, pixLow, pixOpen, pixClose, halfSize, pen); } } } } } }
/// <summary> /// The Copy Constructor /// </summary> /// <param name="rhs">The <see cref="OHLCBarItem"/> object from which to copy</param> public OHLCBarItem( OHLCBarItem rhs ) : base(rhs) { _bar = rhs._bar.Clone(); }
/// <summary> /// Draw all the <see cref="OHLCBar"/>'s to the specified <see cref="Graphics"/> /// device as a candlestick at each defined point. /// </summary> /// <param name="g"> /// A graphic device object to be drawn into. This is normally e.Graphics from the /// PaintEventArgs argument to the Paint() method. /// </param> /// <param name="pane"> /// A reference to the <see cref="GraphPane"/> object that is the parent or /// owner of this object. /// </param> /// <param name="curve">A <see cref="OHLCBarItem"/> object representing the /// <see cref="OHLCBar"/>'s to be drawn.</param> /// <param name="baseAxis">The <see cref="Axis"/> class instance that defines the base (independent) /// axis for the <see cref="OHLCBar"/></param> /// <param name="valueAxis">The <see cref="Axis"/> class instance that defines the value (dependent) /// axis for the <see cref="OHLCBar"/></param> /// <param name="scaleFactor"> /// The scaling factor to be used for rendering objects. This is calculated and /// passed down by the parent <see cref="GraphPane"/> object using the /// <see cref="PaneBase.CalcScaleFactor"/> method, and is used to proportionally adjust /// font sizes, etc. according to the actual size of the graph. /// </param> public virtual void Draw(Graphics g, GraphPane pane, OHLCBarItem curve, Axis baseAxis, Axis valueAxis, float scaleFactor) { //ValueHandler valueHandler = new ValueHandler( pane, false ); if (curve.Points == null) { return; } //float halfSize = _size * scaleFactor; var halfSize = GetBarWidth(pane, baseAxis, scaleFactor); var dotHalfSize = Math.Max(curve.DotHalfSize, IsAutoSize ? Math.Max(2, halfSize / 4) : curve.DotHalfSize) * scaleFactor; using (var pen = curve.IsSelected ? new Pen(Selection.Border.Color, Selection.Border.Width) : new Pen(Color, Width)) using (var fallingPen = curve.IsSelected ? new Pen(Selection.Border.Color, Selection.Border.Width) : new Pen(FallingColor, Width)) { // Loop over each defined point for (int i = 0; i < curve.Points.Count; i++) { var pt = curve.Points[i]; double date; double open; double high; double low; double close; GetOHLC(pt, out date, out open, out high, out low, out close); // Any value set to double max is invalid and should be skipped // This is used for calculated values that are out of range, divide // by zero, etc. // Also, any value <= zero on a log scale is invalid if (curve.Points[i].IsInvalid || (!(date > 0) && baseAxis.Scale.IsLog) || ((!(high > 0) || !(low > 0)) && valueAxis.Scale.IsLog)) { continue; } var pixBase = (int)(baseAxis.Scale.Transform(curve.IsOverrideOrdinal, i, date) + 0.5); //pixBase = baseAxis.Scale.Transform( curve.IsOverrideOrdinal, i, date ); var pixHigh = valueAxis.Scale.Transform(curve.IsOverrideOrdinal, i, high); var pixLow = valueAxis.Scale.Transform(curve.IsOverrideOrdinal, i, low); var pixOpen = PointPairBase.IsValueInvalid(open) ? float.MaxValue : valueAxis.Scale.Transform(curve.IsOverrideOrdinal, i, open); var pixClose = PointPair.IsValueInvalid(close) ? float.MaxValue : valueAxis.Scale.Transform(curve.IsOverrideOrdinal, i, close); var rising = close > open; if (pixBase == PointPair.Missing) { continue; } BeforeDraw(g, pane, valueAxis, curve, pt, pixBase, pixHigh, pixLow, halfSize); var gradient = !curve.IsSelected && this.GradientFill.IsGradientValueType; if (gradient) { using (var tPen = GetPen(pane, scaleFactor, pt)) Draw(g, pane, baseAxis is IXAxis, pixBase, pixHigh, pixLow, pixOpen, pixClose, halfSize, tPen, dotHalfSize); } else { Draw(g, pane, baseAxis is IXAxis, pixBase, pixHigh, pixLow, pixOpen, pixClose, halfSize, rising ? pen : fallingPen, dotHalfSize); } } } }
/// <summary> /// Draw all the <see cref="JapaneseCandleStick"/>'s to the specified <see cref="Graphics"/> /// device as a candlestick at each defined point. /// </summary> /// <param name="g"> /// A graphic device object to be drawn into. This is normally e.Graphics from the /// PaintEventArgs argument to the Paint() method. /// </param> /// <param name="pane"> /// A reference to the <see cref="GraphPane"/> object that is the parent or /// owner of this object. /// </param> /// <param name="curve">A <see cref="JapaneseCandleStickItem"/> object representing the /// <see cref="JapaneseCandleStick"/>'s to be drawn.</param> /// <param name="baseAxis">The <see cref="Axis"/> class instance that defines the base (independent) /// axis for the <see cref="JapaneseCandleStick"/></param> /// <param name="valueAxis">The <see cref="Axis"/> class instance that defines the value (dependent) /// axis for the <see cref="JapaneseCandleStick"/></param> /// <param name="scaleFactor"> /// The scaling factor to be used for rendering objects. This is calculated and /// passed down by the parent <see cref="GraphPane"/> object using the /// <see cref="PaneBase.CalcScaleFactor"/> method, and is used to proportionally adjust /// font sizes, etc. according to the actual size of the graph. /// </param> public override void Draw(Graphics g, GraphPane pane, OHLCBarItem curve, Axis baseAxis, Axis valueAxis, float scaleFactor) { //ValueHandler valueHandler = new ValueHandler( pane, false ); if (curve.Points == null) { return; } //float halfSize = _size * scaleFactor; var halfSize = GetBarWidth(pane, baseAxis, scaleFactor); var dotHalfSize = Math.Max(curve.DotHalfSize, IsAutoSize ? Math.Max(2, halfSize / 4) : curve.DotHalfSize) * scaleFactor; var tColor = Color; var tFallingColor = FallingColor; var tPenWidth = Width; var tRisingFill = RisingFill; var tFallingFill = FallingFill; var tRisingBorder = RisingBorder; var tFallingBorder = FallingBorder; if (curve.IsSelected) { tColor = Selection.Border.Color; tFallingColor = Selection.Border.Color; tPenWidth = Selection.Border.Width; tRisingFill = Selection.Fill; tFallingFill = Selection.Fill; tRisingBorder = Selection.Border; tFallingBorder = Selection.Border; } using (var risingPen = curve.IsSelected ? new Pen(Selection.Border.Color, Selection.Border.Width) : new Pen(tColor, tPenWidth)) using (var fallingPen = curve.IsSelected ? new Pen(Selection.Border.Color, Selection.Border.Width) : new Pen(tFallingColor, tPenWidth)) { // Loop over each defined point for (int i = 0; i < curve.Points.Count; i++) { var pt = curve.Points[i]; double date; double open; double high; double low; double close; GetOHLC(pt, out date, out open, out high, out low, out close); curve.OnBeforeDrawEvent(this, i); // Any value set to double max is invalid and should be skipped // This is used for calculated values that are out of range, divide // by zero, etc. // Also, any value <= zero on a log scale is invalid if (curve.Points[i].IsInvalid || (date <= 0 && baseAxis.Scale.IsLog) || ((high <= 0 || low <= 0) && valueAxis.Scale.IsLog)) { continue; } float pixBase = (int)(baseAxis.Scale.Transform(curve.IsOverrideOrdinal, i, date) + 0.5); //pixBase = baseAxis.Scale.Transform( curve.IsOverrideOrdinal, i, date ); var pixHigh = valueAxis.Scale.Transform(curve.IsOverrideOrdinal, i, high); var pixLow = valueAxis.Scale.Transform(curve.IsOverrideOrdinal, i, low); var pixOpen = PointPair.IsValueInvalid(open) ? float.MaxValue : valueAxis.Scale.Transform(curve.IsOverrideOrdinal, i, open); var pixClose = PointPair.IsValueInvalid(close) ? float.MaxValue : valueAxis.Scale.Transform(curve.IsOverrideOrdinal, i, close); var rising = close > open; if (this.GradientFill.IsGradientValueType) { using (var tPen = GetPen(pane, scaleFactor, pt)) Draw(g, pane, baseAxis is IXAxis, pixBase, pixHigh, pixLow, pixOpen, pixClose, halfSize, scaleFactor, (tPen), (rising ? tRisingFill : tFallingFill), (rising ? tRisingBorder : tFallingBorder), pt, dotHalfSize); } else { Draw(g, pane, baseAxis is IXAxis, pixBase, pixHigh, pixLow, pixOpen, pixClose, halfSize, scaleFactor, (rising ? risingPen : fallingPen), (rising ? tRisingFill : tFallingFill), (rising ? tRisingBorder : tFallingBorder), pt, dotHalfSize); } } } }
/// <summary> /// Draw all the <see cref="OHLCBar"/>'s to the specified <see cref="Graphics"/> /// device as a candlestick at each defined point. /// </summary> /// <param name="g"> /// A graphic device object to be drawn into. This is normally e.Graphics from the /// PaintEventArgs argument to the Paint() method. /// </param> /// <param name="pane"> /// A reference to the <see cref="GraphPane"/> object that is the parent or /// owner of this object. /// </param> /// <param name="curve">A <see cref="OHLCBarItem"/> object representing the /// <see cref="OHLCBar"/>'s to be drawn.</param> /// <param name="baseAxis">The <see cref="Axis"/> class instance that defines the base (independent) /// axis for the <see cref="OHLCBar"/></param> /// <param name="valueAxis">The <see cref="Axis"/> class instance that defines the value (dependent) /// axis for the <see cref="OHLCBar"/></param> /// <param name="scaleFactor"> /// The scaling factor to be used for rendering objects. This is calculated and /// passed down by the parent <see cref="GraphPane"/> object using the /// <see cref="PaneBase.CalcScaleFactor"/> method, and is used to proportionally adjust /// font sizes, etc. according to the actual size of the graph. /// </param> public void Draw(Graphics g, GraphPane pane, OHLCBarItem curve, Axis baseAxis, Axis valueAxis, float scaleFactor) { //ValueHandler valueHandler = new ValueHandler( pane, false ); float pixBase, pixHigh, pixLow, pixOpen, pixClose; if (curve.Points != null) { //float halfSize = _size * scaleFactor; float halfSize = Math.Max(1, GetBarWidth(pane, baseAxis, scaleFactor)); int minX = int.MinValue; int maxX = int.MaxValue; int minY = int.MinValue; int maxY = int.MaxValue; if (pane != null) { minX = (int)pane.Chart.Rect.Left; maxX = (int)pane.Chart.Rect.Right; minY = (int)pane.Chart.Rect.Top; maxY = (int)pane.Chart.Rect.Bottom; } if (isOptDraw) { if (isPixelDrawn == null) { isPixelDrawn = new Dictionary <long, bool>(); } else { isPixelDrawn.Clear(); } } using (Pen pen = !curve.IsSelected ? new Pen(_color, _width) : new Pen(Selection.Border.Color, Selection.Border.Width)) // using ( Pen pen = new Pen( _color, _penWidth ) ) { double date; double high; double low; double open; double close; StockPt pt; int minOrdinal = 0; int maxOrdinal = int.MaxValue; double minScale = baseAxis.Scale.Min; double maxScale = baseAxis.Scale.Max; var increment = 1; if (baseAxis.Scale.IsAnyOrdinal && !curve.IsOverrideOrdinal) { minOrdinal = (int)baseAxis.Scale.Min; maxOrdinal = (int)baseAxis.Scale.Max; var ordinalWidth = maxOrdinal - minOrdinal + 1; var pixelWidth = maxX - minX + 1; var quotient = ordinalWidth / pixelWidth; if (quotient > 1) { increment = quotient; } } else if (curve.Points.Count > 100000) { pt = curve.Points[0] as StockPt; if (pt != null) { double firstDate = pt.X; pt = curve.Points[curve.Points.Count - 1] as StockPt; if (pt != null) { double lastDate = pt.X; double scalePerOrdinal = (lastDate - firstDate) / curve.Points.Count; minOrdinal = (int)Math.Max(0, (minScale - firstDate) / scalePerOrdinal); maxOrdinal = (int)((maxScale - firstDate) / scalePerOrdinal); int estimateWidth = Math.Max(100000, maxOrdinal - minOrdinal); minOrdinal = Math.Max(0, minOrdinal - estimateWidth); maxOrdinal += estimateWidth; } } } // Loop over each defined point var limit = Math.Min(curve.Points.Count, maxOrdinal); for (int i = Math.Max(minOrdinal, 0); i < limit; i++) { pt = curve.Points[i] as StockPt; if (pt == null) { continue; } date = pt.X; if (!baseAxis.Scale.IsAnyOrdinal || curve.IsOverrideOrdinal) { if (date < minScale || date > maxScale) { continue; } } open = (pt as StockPt).Open; close = (pt as StockPt).Close; pixBase = (int)(baseAxis.Scale.Transform(curve.IsOverrideOrdinal, i, date) + 0.5); if (pixBase < minX || pixBase > maxX) { // Skip this one, it's outside the visible scroll range. continue; } if (PointPair.IsValueInvalid(close)) { pixClose = Single.MaxValue; } else { pixClose = valueAxis.Scale.Transform(curve.IsOverrideOrdinal, i, close); } high = pt.Y; low = pt.Z; // Any value set to double max is invalid and should be skipped // This is used for calculated values that are out of range, divide // by zero, etc. // Also, any value <= zero on a log scale is invalid if (!curve.Points[i].IsInvalid3D && (date > 0 || !baseAxis._scale.IsLog) && ((high > 0 && low > 0) || !valueAxis._scale.IsLog)) { pixHigh = valueAxis.Scale.Transform(curve.IsOverrideOrdinal, i, high); pixLow = valueAxis.Scale.Transform(curve.IsOverrideOrdinal, i, low); if (PointPair.IsValueInvalid(open)) { pixOpen = Single.MaxValue; } else { pixOpen = valueAxis.Scale.Transform(curve.IsOverrideOrdinal, i, open); } // Don't try to draw where we already drew. // This is a huge optimization when there are // many more draw items than pixels in the rectangle. int xOpt = (int)(maxX - pixBase); int yOptHigh = (int)(maxY - pixHigh); int yOptLow = (int)(maxY - pixLow); int yOpt = yOptLow <0 || yOptLow> maxY ? yOptHigh : yOptLow; bool value; if (isOptDraw) { if (xOpt > maxX || yOpt > maxY || xOpt < 0 || yOpt < 0 || isPixelDrawn.TryGetValue(GetHashCode(xOpt, yOpt), out value)) { continue; } else { isPixelDrawn.Add(GetHashCode(xOpt, yOpt), true); } } if (!curve.IsSelected && this._gradientFill.IsGradientValueType) { using (Pen tPen = GetPen(pane, scaleFactor, pt)) Draw(g, pane, baseAxis is XAxis || baseAxis is X2Axis, pixBase, pixHigh, pixLow, pixOpen, pixClose, halfSize, tPen); } else { Draw(g, pane, baseAxis is XAxis || baseAxis is X2Axis, pixBase, pixHigh, pixLow, pixOpen, pixClose, halfSize, pen); } } } } } }