forked from tsai/mathnet-numerics
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
612 lines
22 KiB
612 lines
22 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics.Contracts;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Xml;
|
|
using System.Xml.Schema;
|
|
using System.Xml.Serialization;
|
|
using MathNet.Numerics.LinearAlgebra;
|
|
using MathNet.Numerics.Spatial.Internal;
|
|
|
|
namespace MathNet.Numerics.Spatial.Euclidean2D
|
|
{
|
|
/// <summary>
|
|
/// A struct representing a vector in 2D space
|
|
/// </summary>
|
|
[Serializable]
|
|
public struct Vector2D : IXmlSerializable, IEquatable<Vector2D>, IFormattable
|
|
{
|
|
/// <summary>
|
|
/// The x component.
|
|
/// </summary>
|
|
public readonly double X;
|
|
|
|
/// <summary>
|
|
/// The y component.
|
|
/// </summary>
|
|
public readonly double Y;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="Vector2D"/> struct.
|
|
/// </summary>
|
|
/// <param name="x">The x component.</param>
|
|
/// <param name="y">The y component.</param>
|
|
public Vector2D(double x, double y)
|
|
{
|
|
this.X = x;
|
|
this.Y = y;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="Vector2D"/> struct.
|
|
/// Creates a vector with length r rotated a counterclockwise from X-Axis
|
|
/// </summary>
|
|
/// <param name="r">The radius</param>
|
|
/// <param name="a">The angle</param>
|
|
[Obsolete("This constructor will be removed, use FromPolar. Made obsolete 2017-12-03.")]
|
|
//// ReSharper disable once UnusedMember.Global
|
|
public Vector2D(double r, Angle a)
|
|
: this(r * Math.Cos(a.Radians), r * Math.Sin(a.Radians))
|
|
{
|
|
if (r < 0)
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(r), r, "Expected a radius greater than or equal to zero.");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="Vector2D"/> struct.
|
|
/// </summary>
|
|
/// <param name="data">A list of 2 doubles</param>
|
|
[Obsolete("This constructor will be removed. Made obsolete 2017-12-03.")]
|
|
//// ReSharper disable once UnusedMember.Global
|
|
public Vector2D(IEnumerable<double> data)
|
|
: this(data.ToArray())
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="Vector2D"/> struct.
|
|
/// </summary>
|
|
/// <param name="data">A list of 2 doubles</param>
|
|
[Obsolete("This constructor will be removed. Made obsolete 2017-12-03.")]
|
|
public Vector2D(double[] data)
|
|
: this(data[0], data[1])
|
|
{
|
|
if (data.Length != 2)
|
|
{
|
|
throw new ArgumentException("data.Length != 2!");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a vector representing the X Axis
|
|
/// </summary>
|
|
public static Vector2D XAxis { get; } = new Vector2D(1, 0);
|
|
|
|
/// <summary>
|
|
/// Gets a vector representing the Y Axis
|
|
/// </summary>
|
|
public static Vector2D YAxis { get; } = new Vector2D(0, 1);
|
|
|
|
/// <summary>
|
|
/// Gets the length of the vector
|
|
/// </summary>
|
|
[Pure]
|
|
public double Length => Math.Sqrt((this.X * this.X) + (this.Y * this.Y));
|
|
|
|
/// <summary>
|
|
/// Returns a value that indicates whether each pair of elements in two specified vectors is equal.
|
|
/// </summary>
|
|
/// <param name="left">The first vector to compare.</param>
|
|
/// <param name="right">The second vector to compare.</param>
|
|
/// <returns>True if the vectors are the same; otherwise false.</returns>
|
|
public static bool operator ==(Vector2D left, Vector2D right)
|
|
{
|
|
return left.Equals(right);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a value that indicates whether any pair of elements in two specified vectors is not equal.
|
|
/// </summary>
|
|
/// <param name="left">The first vector to compare.</param>
|
|
/// <param name="right">The second vector to compare.</param>
|
|
/// <returns>True if the vectors are different; otherwise false.</returns>
|
|
public static bool operator !=(Vector2D left, Vector2D right)
|
|
{
|
|
return !left.Equals(right);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds two vectors
|
|
/// </summary>
|
|
/// <param name="left">The first vector</param>
|
|
/// <param name="right">The second vector</param>
|
|
/// <returns>A new summed vector</returns>
|
|
public static Vector2D operator +(Vector2D left, Vector2D right)
|
|
{
|
|
return left.Add(right);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Subtracts two vectors
|
|
/// </summary>
|
|
/// <param name="left">The first vector</param>
|
|
/// <param name="right">The second vector</param>
|
|
/// <returns>A new difference vector</returns>
|
|
public static Vector2D operator -(Vector2D left, Vector2D right)
|
|
{
|
|
return left.Subtract(right);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Negates the vector
|
|
/// </summary>
|
|
/// <param name="v">A vector to negate</param>
|
|
/// <returns>A new negated vector</returns>
|
|
public static Vector2D operator -(Vector2D v)
|
|
{
|
|
return v.Negate();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Multiplies a vector by a scalar
|
|
/// </summary>
|
|
/// <param name="d">A scalar</param>
|
|
/// <param name="v">A vector</param>
|
|
/// <returns>A scaled vector</returns>
|
|
public static Vector2D operator *(double d, Vector2D v)
|
|
{
|
|
return new Vector2D(d * v.X, d * v.Y);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Multiplies a vector by a scalar
|
|
/// </summary>
|
|
/// <param name="v">A vector</param>
|
|
/// <param name="d">A scalar</param>
|
|
/// <returns>A scaled vector</returns>
|
|
public static Vector2D operator *(Vector2D v, double d)
|
|
{
|
|
return d * v;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Divides a vector by a scalar
|
|
/// </summary>
|
|
/// <param name="v">A vector</param>
|
|
/// <param name="d">A scalar</param>
|
|
/// <returns>A scaled vector</returns>
|
|
public static Vector2D operator /(Vector2D v, double d)
|
|
{
|
|
return new Vector2D(v.X / d, v.Y / d);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a Vector from Polar coordinates
|
|
/// </summary>
|
|
/// <param name="radius">The distance of the point from the origin</param>
|
|
/// <param name="angle">The angle of the point as measured from the X Axis</param>
|
|
/// <returns>A vector.</returns>
|
|
public static Vector2D FromPolar(double radius, Angle angle)
|
|
{
|
|
if (radius < 0)
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(radius), radius, "Expected a radius greater than or equal to zero.");
|
|
}
|
|
|
|
return new Vector2D(
|
|
radius * Math.Cos(angle.Radians),
|
|
radius * Math.Sin(angle.Radians));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempts to convert a string of the form x,y into a point
|
|
/// </summary>
|
|
/// <param name="text">The string to be converted</param>
|
|
/// <param name="result">A point at the coordinates specified</param>
|
|
/// <returns>True if <paramref name="text"/> could be parsed.</returns>
|
|
public static bool TryParse(string text, out Vector2D result)
|
|
{
|
|
return TryParse(text, null, out result);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempts to convert a string of the form x,y into a point
|
|
/// </summary>
|
|
/// <param name="text">The string to be converted</param>
|
|
/// <param name="formatProvider">The <see cref="IFormatProvider"/></param>
|
|
/// <param name="result">A point at the coordinates specified</param>
|
|
/// <returns>True if <paramref name="text"/> could be parsed.</returns>
|
|
public static bool TryParse(string text, IFormatProvider formatProvider, out Vector2D result)
|
|
{
|
|
if (Text.TryParse2D(text, formatProvider, out var x, out var y))
|
|
{
|
|
result = new Vector2D(x, y);
|
|
return true;
|
|
}
|
|
|
|
result = default(Vector2D);
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempts to convert a string of the form x,y into a point
|
|
/// </summary>
|
|
/// <param name="value">The string to be converted</param>
|
|
/// <param name="formatProvider">The <see cref="IFormatProvider"/></param>
|
|
/// <returns>A point at the coordinates specified</returns>
|
|
public static Vector2D Parse(string value, IFormatProvider formatProvider = null)
|
|
{
|
|
if (TryParse(value, formatProvider, out var p))
|
|
{
|
|
return p;
|
|
}
|
|
|
|
throw new FormatException($"Could not parse a Vector2D from the string {value}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates an <see cref="Vector2D"/> from an <see cref="XmlReader"/>.
|
|
/// </summary>
|
|
/// <param name="reader">An <see cref="XmlReader"/> positioned at the node to read into this <see cref="Vector2D"/>.</param>
|
|
/// <returns>An <see cref="Vector2D"/> that contains the data read from the reader.</returns>
|
|
public static Vector2D ReadFrom(XmlReader reader)
|
|
{
|
|
return reader.ReadElementAs<Vector2D>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a new <see cref="Vector2D"/> from a Math.NET Numerics vector of length 2.
|
|
/// </summary>
|
|
/// <param name="vector"> A vector with length 2 to populate the created instance with.</param>
|
|
/// <returns> A <see cref="Vector2D"/></returns>
|
|
[Pure]
|
|
public static Vector2D OfVector(Vector<double> vector)
|
|
{
|
|
if (vector.Count != 2)
|
|
{
|
|
throw new ArgumentException("The vector length must be 2 in order to convert it to a Vector2D");
|
|
}
|
|
|
|
return new Vector2D(vector.At(0), vector.At(1));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Computes whether or not this vector is perpendicular to <paramref name="other"/> vector by:
|
|
/// 1. Normalizing both
|
|
/// 2. Computing the dot product.
|
|
/// 3. Comparing 1- Math.Abs(dot product) to <paramref name="tolerance"/>
|
|
/// </summary>
|
|
/// <param name="other">The other <see cref="Vector2D"/></param>
|
|
/// <param name="tolerance">The tolerance for when vectors are said to be parallel</param>
|
|
/// <returns>True if the vector dot product is within the given double tolerance of unity, false if not</returns>
|
|
[Pure]
|
|
public bool IsParallelTo(Vector2D other, double tolerance = 1e-10)
|
|
{
|
|
var dp = Math.Abs(this.Normalize().DotProduct(other.Normalize()));
|
|
return Math.Abs(1 - dp) <= tolerance;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Computes whether or not this vector is parallel to another vector within a given angle tolerance.
|
|
/// </summary>
|
|
/// <param name="other">The other <see cref="Vector2D"/></param>
|
|
/// <param name="tolerance">The tolerance for when vectors are said to be parallel</param>
|
|
/// <returns>True if the vectors are parallel within the angle tolerance, false if they are not</returns>
|
|
[Pure]
|
|
public bool IsParallelTo(Vector2D other, Angle tolerance)
|
|
{
|
|
// Compute the angle between these vectors
|
|
var angle = this.AngleTo(other);
|
|
if (angle < tolerance)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Compute the 180° opposite of the angle
|
|
return Angle.FromRadians(Math.PI) - angle < tolerance;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Computes whether or not this vector is perpendicular to <paramref name="other"/> vector by:
|
|
/// 1. Normalizing both
|
|
/// 2. Computing the dot product.
|
|
/// 3. Comparing Math.Abs(dot product) to <paramref name="tolerance"/>
|
|
/// </summary>
|
|
/// <param name="other">The other <see cref="Vector2D"/></param>
|
|
/// <param name="tolerance">The tolerance for when vectors are said to be parallel</param>
|
|
/// <returns>True if the vector dot product is within the given double tolerance of unity, false if not</returns>
|
|
[Pure]
|
|
public bool IsPerpendicularTo(Vector2D other, double tolerance = 1e-10)
|
|
{
|
|
return Math.Abs(this.Normalize().DotProduct(other.Normalize())) < tolerance;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Computes whether or not this vector is parallel to another vector within a given angle tolerance.
|
|
/// </summary>
|
|
/// <param name="other">The other <see cref="Vector2D"/></param>
|
|
/// <param name="tolerance">The tolerance for when vectors are said to be parallel</param>
|
|
/// <returns>True if the vectors are parallel within the angle tolerance, false if they are not</returns>
|
|
[Pure]
|
|
public bool IsPerpendicularTo(Vector2D other, Angle tolerance)
|
|
{
|
|
var angle = this.AngleTo(other);
|
|
const double Perpendicular = Math.PI / 2;
|
|
return Math.Abs(angle.Radians - Perpendicular) < tolerance.Radians;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Compute the signed angle to another vector.
|
|
/// </summary>
|
|
/// <param name="other">The other <see cref="Vector2D"/></param>
|
|
/// <param name="clockWise">Positive in clockwise direction</param>
|
|
/// <param name="returnNegative">When true and the result is > 180° a negative value is returned</param>
|
|
/// <returns>The angle between the vectors.</returns>
|
|
[Pure]
|
|
public Angle SignedAngleTo(Vector2D other, bool clockWise = false, bool returnNegative = false)
|
|
{
|
|
var sign = clockWise ? -1 : 1;
|
|
var a1 = Math.Atan2(this.Y, this.X);
|
|
if (a1 < 0)
|
|
{
|
|
a1 += 2 * Math.PI;
|
|
}
|
|
|
|
var a2 = Math.Atan2(other.Y, other.X);
|
|
if (a2 < 0)
|
|
{
|
|
a2 += 2 * Math.PI;
|
|
}
|
|
|
|
var a = sign * (a2 - a1);
|
|
if (a < 0 && !returnNegative)
|
|
{
|
|
a += 2 * Math.PI;
|
|
}
|
|
|
|
if (a > Math.PI && returnNegative)
|
|
{
|
|
a -= 2 * Math.PI;
|
|
}
|
|
|
|
return Angle.FromRadians(a);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Compute the angle between this vector and another using the arccosine of the dot product.
|
|
/// </summary>
|
|
/// <param name="other">The other <see cref="Vector2D"/></param>
|
|
/// <returns>The angle between vectors, with a range between 0° and 180°</returns>
|
|
[Pure]
|
|
public Angle AngleTo(Vector2D other)
|
|
{
|
|
return Angle.FromRadians(
|
|
Math.Abs(
|
|
Math.Atan2(
|
|
(this.X * other.Y) - (other.X * this.Y),
|
|
(this.X * other.X) + (this.Y * other.Y))));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Rotates a Vector by an angle
|
|
/// </summary>
|
|
/// <param name="angle">The angle.</param>
|
|
/// <returns>A new rotated vector.</returns>
|
|
[Pure]
|
|
public Vector2D Rotate(Angle angle)
|
|
{
|
|
var cs = Math.Cos(angle.Radians);
|
|
var sn = Math.Sin(angle.Radians);
|
|
var x = (this.X * cs) - (this.Y * sn);
|
|
var y = (this.X * sn) + (this.Y * cs);
|
|
return new Vector2D(x, y);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Perform the dot product on a pair of vectors
|
|
/// </summary>
|
|
/// <param name="other">The second vector</param>
|
|
/// <returns>The result of the dot product.</returns>
|
|
[Pure]
|
|
public double DotProduct(Vector2D other)
|
|
{
|
|
return (this.X * other.X) + (this.Y * other.Y);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Performs the 2D 'cross product' as if the 2D vectors were really 3D vectors in the z=0 plane, returning
|
|
/// the scalar magnitude and direction of the resulting z value.
|
|
/// Formula: (this.X * other.Y) - (this.Y * other.X)
|
|
/// </summary>
|
|
/// <param name="other">The other <see cref="Vector2D"/></param>
|
|
/// <returns>(this.X * other.Y) - (this.Y * other.X)</returns>
|
|
[Pure]
|
|
public double CrossProduct(Vector2D other)
|
|
{
|
|
// Though the cross product is undefined in 2D space, this is a useful mathematical operation to
|
|
// determine angular direction and to compute the area of 2D shapes
|
|
return (this.X * other.Y) - (this.Y * other.X);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Projects this vector onto another vector
|
|
/// </summary>
|
|
/// <param name="other">The other <see cref="Vector2D"/></param>
|
|
/// <returns>A <see cref="Vector2D"/> representing this vector projected on <paramref name="other"/></returns>
|
|
[Pure]
|
|
public Vector2D ProjectOn(Vector2D other)
|
|
{
|
|
return other * (this.DotProduct(other) / other.DotProduct(other));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new unit vector from the existing vector.
|
|
/// </summary>
|
|
/// <returns>A new unit vector in the same direction as the original vector</returns>
|
|
[Pure]
|
|
public Vector2D Normalize()
|
|
{
|
|
var l = this.Length;
|
|
return new Vector2D(this.X / l, this.Y / l);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Scales the vector by the provided value
|
|
/// </summary>
|
|
/// <param name="d">a scaling factor</param>
|
|
/// <returns>A new scale adjusted vector</returns>
|
|
[Pure]
|
|
public Vector2D ScaleBy(double d)
|
|
{
|
|
return new Vector2D(d * this.X, d * this.Y);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the negative of the vector
|
|
/// </summary>
|
|
/// <returns>A new negated vector.</returns>
|
|
[Pure]
|
|
public Vector2D Negate()
|
|
{
|
|
return new Vector2D(-1 * this.X, -1 * this.Y);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Subtracts a vector from this vector.
|
|
/// </summary>
|
|
/// <param name="v">A vector to subtract</param>
|
|
/// <returns>A new vector which is the difference of the current vector and the provided vector</returns>
|
|
[Pure]
|
|
public Vector2D Subtract(Vector2D v)
|
|
{
|
|
return new Vector2D(this.X - v.X, this.Y - v.Y);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a vector to this vector
|
|
/// </summary>
|
|
/// <param name="v">A vector to add</param>
|
|
/// <returns>A new vector which is the sum of the existing vector and the provided vector</returns>
|
|
[Pure]
|
|
public Vector2D Add(Vector2D v)
|
|
{
|
|
return new Vector2D(this.X + v.X, this.Y + v.Y);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Transforms a vector by multiplying it against a provided matrix
|
|
/// </summary>
|
|
/// <param name="m">The matrix to multiply</param>
|
|
/// <returns>A new transformed vector</returns>
|
|
[Pure]
|
|
public Vector2D TransformBy(Matrix<double> m)
|
|
{
|
|
var transformed = m.Multiply(this.ToVector());
|
|
return new Vector2D(transformed.At(0), transformed.At(1));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Convert to a Math.NET Numerics dense vector of length 2.
|
|
/// </summary>
|
|
/// <returns> A <see cref="Vector{Double}"/> with the x and y values from this instance.</returns>
|
|
[Pure]
|
|
public Vector<double> ToVector()
|
|
{
|
|
return Vector<double>.Build.Dense(new[] { this.X, this.Y });
|
|
}
|
|
|
|
/// <summary>
|
|
/// Compare this instance with <paramref name="other"/>
|
|
/// </summary>
|
|
/// <param name="other">The other <see cref="Vector2D"/></param>
|
|
/// <param name="tolerance">The tolerance when comparing the x and y components</param>
|
|
/// <returns>True if found to be equal.</returns>
|
|
[Pure]
|
|
public bool Equals(Vector2D other, double tolerance)
|
|
{
|
|
if (tolerance < 0)
|
|
{
|
|
throw new ArgumentException("epsilon < 0");
|
|
}
|
|
|
|
return Math.Abs(other.X - this.X) < tolerance &&
|
|
Math.Abs(other.Y - this.Y) < tolerance;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
[Pure]
|
|
public bool Equals(Vector2D other) => this.X.Equals(other.X) && this.Y.Equals(other.Y);
|
|
|
|
/// <inheritdoc />
|
|
[Pure]
|
|
public override bool Equals(object obj) => obj is Vector2D v && this.Equals(v);
|
|
|
|
/// <inheritdoc />
|
|
[Pure]
|
|
public override int GetHashCode() => HashCode.Combine(this.X, this.Y);
|
|
|
|
/// <inheritdoc />
|
|
[Pure]
|
|
public override string ToString()
|
|
{
|
|
return this.ToString(null, CultureInfo.InvariantCulture);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a string representation of this instance using the provided <see cref="IFormatProvider"/>
|
|
/// </summary>
|
|
/// <param name="provider">A <see cref="IFormatProvider"/></param>
|
|
/// <returns>The string representation of this instance.</returns>
|
|
[Pure]
|
|
public string ToString(IFormatProvider provider)
|
|
{
|
|
return this.ToString(null, provider);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
[Pure]
|
|
public string ToString(string format, IFormatProvider provider = null)
|
|
{
|
|
var numberFormatInfo = provider != null ? NumberFormatInfo.GetInstance(provider) : CultureInfo.InvariantCulture.NumberFormat;
|
|
var separator = numberFormatInfo.NumberDecimalSeparator == "," ? ";" : ",";
|
|
return $"({this.X.ToString(format, numberFormatInfo)}{separator}\u00A0{this.Y.ToString(format, numberFormatInfo)})";
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
XmlSchema IXmlSerializable.GetSchema()
|
|
{
|
|
return null;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
void IXmlSerializable.ReadXml(XmlReader reader)
|
|
{
|
|
if (reader.TryReadAttributeAsDouble("X", out var x) &&
|
|
reader.TryReadAttributeAsDouble("Y", out var y))
|
|
{
|
|
reader.Skip();
|
|
this = new Vector2D(x, y);
|
|
return;
|
|
}
|
|
|
|
if (reader.TryReadChildElementsAsDoubles("X", "Y", out x, out y))
|
|
{
|
|
reader.Skip();
|
|
this = new Vector2D(x, y);
|
|
return;
|
|
}
|
|
|
|
throw new XmlException("Could not read a Vector2D");
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
void IXmlSerializable.WriteXml(XmlWriter writer)
|
|
{
|
|
writer.WriteAttribute("X", this.X);
|
|
writer.WriteAttribute("Y", this.Y);
|
|
}
|
|
}
|
|
}
|
|
|