Advertisement
creamygoat

Visualising the Racetrack for udacity CS373 hw5-4

May 17th, 2012
383
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 18.58 KB | None | 0 0
  1. #!/usr/bin/python
  2. # -*- coding: utf-8 -*-
  3. #-------------------------------------------------------------------------------
  4. # Racetrack following with PID control for hw5-4
  5. #
  6. # hw0504_racetrack_svg.py
  7. # http://pastebin.com/4nTNtLmm
  8. #
  9. # Custom modules:
  10. #   vegesvgplot.py        http://pastebin.com/6Aek3Exm
  11. #
  12. #-------------------------------------------------------------------------------
  13.  
  14.  
  15. '''Racetrack following with PID control for hw5-4
  16.  
  17. Description:
  18.  This Python script demonstrates the udacity CS373 robot car following
  19.  a racetrack. The output is written to a Scalable Vector Graphic file
  20.  named “output.svg”.
  21.  
  22. Author(s):
  23.  Daniel Neville
  24.  Prof. Sebastian Thrun, udacity (original robot simulation)
  25.  
  26. Copyright, Licence:
  27.  Code by Daniel Neville: Public domain
  28.  Code snarfed from udacity: See http://www.udacity.com/legal/
  29.  
  30. Platform:
  31.  Python 2.5
  32.  
  33.  
  34. INDEX
  35.  
  36.  
  37. Imports
  38.  
  39. Fun stuff:
  40.  
  41.  RunUnitCode(PIDParams, Radius, [ShowProgress], [PathLog])
  42.    (Snarfed and modified robot code)
  43.  RenderToSVG(Data)
  44.  TwiddleAndPlot(InitialCTE, Tolerance, YScale, ErrFnIx, Data)
  45.  
  46. Main:
  47.  
  48.  Main()
  49.  
  50. '''
  51.  
  52.  
  53. #-------------------------------------------------------------------------------
  54.  
  55.  
  56. import math
  57.  
  58. from math import (
  59.   pi, sqrt, hypot, sin, cos, tan, asin, acos, atan, atan2, radians, degrees,
  60.   floor, ceil
  61. )
  62.  
  63. import random
  64.  
  65. # The SVG Plotting for Vegetables module can be found at
  66. # http://pastebin.com/6Aek3Exm
  67.  
  68. from vegesvgplot import (
  69.  
  70.   # Shape constants
  71.   Pt_Break, Pt_Anchor, Pt_Control,
  72.   PtCmdWithCoordsSet, PtCmdSet,
  73.  
  74.   # Indent tracker class
  75.   tIndentTracker,
  76.  
  77.   # Affine matrix class
  78.   tAffineMtx,
  79.  
  80.   # Affine matrix creation functions
  81.   AffineMtxTS, AffineMtxTRS2D, Affine2DMatrices,
  82.  
  83.   # Utility functions
  84.   ValidatedRange, MergedDictionary, Save,
  85.   ArrayDimensions, NewMDArray, CopyArray, At, SetAt,
  86.  
  87.   # Basic vector functions
  88.   VZeros, VOnes, VStdBasis, VDim, VAug, VMajorAxis,
  89.   VNeg, VSum, VDiff, VSchur, VDot,
  90.   VLengthSquared, VLength, VManhattan,
  91.   VScaled, VNormalised,
  92.   VPerp, VCrossProduct, VCrossProduct4D,
  93.   VScalarTripleProduct, VVectorTripleProduct,
  94.   VProjectionOnto,
  95.   VTransposedMAV,
  96.   VRectToPol, VPolToRect,
  97.   VLerp,
  98.  
  99.   # Shape functions
  100.   ShapeFromVertices, ShapePoints, ShapeSubpathRanges, ShapeCurveRanges,
  101.   ShapeLength, LineToShapeIntersections, TransformedShape, PiecewiseArc,
  102.  
  103.   # Output formatting functions
  104.   MaxDP, GFListStr, GFTupleStr, HTMLEscaped, AttrMarkup, ProgressColourStr,
  105.  
  106.   # SVG functions
  107.   SVGStart, SVGEnd, SVGPathDataSegments, SVGPath, SVGText,
  108.   SVGGroup, SVGGroupEnd, SVGGrid
  109.  
  110. )
  111.  
  112.  
  113. #-------------------------------------------------------------------------------
  114. # Fun stuff
  115. #-------------------------------------------------------------------------------
  116.  
  117.  
  118. def RunUnitCode(PIDParams, Radius, ShowProgress=False, PathLog=None):
  119.  
  120.   if PathLog is None:
  121.     PathLog = []
  122.  
  123.   #-----------------------------------------------------------------------------
  124.   # Code snarfed from udacity and modified
  125.   #-----------------------------------------------------------------------------
  126.  
  127.   # ------------------------------------------------
  128.   #
  129.   # this is the robot class
  130.   #
  131.  
  132.   class robot:
  133.  
  134.       # --------
  135.       # init:
  136.       #    creates robot and initializes location/orientation to 0, 0, 0
  137.       #
  138.  
  139.       def __init__(self, length = 20.0):
  140.           self.x = 0.0
  141.           self.y = 0.0
  142.           self.orientation = 0.0
  143.           self.length = length
  144.           self.steering_noise = 0.0
  145.           self.distance_noise = 0.0
  146.           self.steering_drift = 0.0
  147.  
  148.       # --------
  149.       # set:
  150.       # sets a robot coordinate
  151.       #
  152.  
  153.       def set(self, new_x, new_y, new_orientation):
  154.  
  155.           self.x = float(new_x)
  156.           self.y = float(new_y)
  157.           self.orientation = float(new_orientation) % (2.0 * pi)
  158.  
  159.  
  160.       # --------
  161.       # set_noise:
  162.       # sets the noise parameters
  163.       #
  164.  
  165.       def set_noise(self, new_s_noise, new_d_noise):
  166.           # makes it possible to change the noise parameters
  167.           # this is often useful in particle filters
  168.           self.steering_noise = float(new_s_noise)
  169.           self.distance_noise = float(new_d_noise)
  170.  
  171.       # --------
  172.       # set_steering_drift:
  173.       # sets the systematical steering drift parameter
  174.       #
  175.  
  176.       def set_steering_drift(self, drift):
  177.           self.steering_drift = drift
  178.  
  179.       # --------
  180.       # move:
  181.       #    steering = front wheel steering angle, limited by max_steering_angle
  182.       #    distance = total distance driven, most be non-negative
  183.  
  184.       def move(self, steering, distance,
  185.                tolerance = 0.001, max_steering_angle = pi / 4.0):
  186.  
  187.           if steering > max_steering_angle:
  188.               steering = max_steering_angle
  189.           if steering < -max_steering_angle:
  190.               steering = -max_steering_angle
  191.           if distance < 0.0:
  192.               distance = 0.0
  193.  
  194.  
  195.           # make a new copy
  196.           res = robot()
  197.           res.length         = self.length
  198.           res.steering_noise = self.steering_noise
  199.           res.distance_noise = self.distance_noise
  200.           res.steering_drift = self.steering_drift
  201.  
  202.           # apply noise
  203.           steering2 = random.gauss(steering, self.steering_noise)
  204.           distance2 = random.gauss(distance, self.distance_noise)
  205.  
  206.           # apply steering drift
  207.           steering2 += self.steering_drift
  208.  
  209.           # Execute motion
  210.           turn = tan(steering2) * distance2 / res.length
  211.  
  212.           if abs(turn) < tolerance:
  213.  
  214.               # approximate by straight line motion
  215.  
  216.               res.x = self.x + (distance2 * cos(self.orientation))
  217.               res.y = self.y + (distance2 * sin(self.orientation))
  218.               res.orientation = (self.orientation + turn) % (2.0 * pi)
  219.  
  220.           else:
  221.  
  222.               # approximate bicycle model for motion
  223.  
  224.               radius = distance2 / turn
  225.               cx = self.x - (sin(self.orientation) * radius)
  226.               cy = self.y + (cos(self.orientation) * radius)
  227.               res.orientation = (self.orientation + turn) % (2.0 * pi)
  228.               res.x = cx + (sin(res.orientation) * radius)
  229.               res.y = cy - (cos(res.orientation) * radius)
  230.  
  231.           return res
  232.  
  233.  
  234.  
  235.  
  236.       def __repr__(self):
  237.           return '[x=%.5f y=%.5f orient=%.5f]'  % (self.x, self.y, self.orientation)
  238.  
  239.  
  240.   ############## ONLY ADD / MODIFY CODE BELOW THIS LINE ####################
  241.  
  242.       def cte(self, radius):
  243.  
  244.           '''Return the cross-track error from the racetrack path.
  245.  
  246.          The racetrack is parametised by the given radius of its semicircular
  247.          sections at each end. The length of the straight sections connecting
  248.          the semicircles is twice the radius.
  249.  
  250.          The car may be travelling around the track in either direction.
  251.  
  252.          '''
  253.  
  254.           #---------------------------------------------------------------------
  255.  
  256.           def VSum(A, B):
  257.             return tuple(x + y for x, y in zip(A, B))
  258.  
  259.           def VDiff(A, B):
  260.             return tuple(x - y for x, y in zip(A, B))
  261.  
  262.           def VDot(A, B):
  263.             return sum(x * y for x, y in zip(A, B))
  264.  
  265.           def VLengthSquared(A):
  266.             return sum(x * x for x in A)
  267.  
  268.           def VLength(A):
  269.             return sqrt(VLengthSquared(A))
  270.  
  271.           def VScaled(A, Scale):
  272.             return tuple(x * Scale for x in A)
  273.  
  274.           def VNeg(A):
  275.             return tuple(-x for x in A)
  276.  
  277.           def VNormalised(A):
  278.             return VScaled(A, 1.0 / VLength(A))
  279.  
  280.           def VPerp(A):
  281.             return (-A[1], A[0])
  282.  
  283.           def VProjectionOnto(Axis, Vector):
  284.             Result = VScaled(
  285.               Axis,
  286.               VDot(Vector, Axis) / (1.0 * LengthSquared(Axis))
  287.             )
  288.             return Result
  289.  
  290.           def VLerp(A, B, Progress):
  291.             return VSum(VScaled(A, 1.0 - Progress), VScaled(B, Progress))
  292.  
  293.           def VRectToPol(A):
  294.             Distance = VLength(A)
  295.             Angle = atan2(A[1], A[0])
  296.             return (Distance, Angle)
  297.  
  298.           def VPolToRect(B):
  299.             c = cos(B[1])
  300.             s = sin(B[1])
  301.             return (B[0] * c, B[0] * s)
  302.  
  303.           #---------------------------------------------------------------------
  304.  
  305.           def CTEFromCircle(Pos, Heading, Centre, Radius):
  306.             Sinister = VPerp(Heading)
  307.             Radial = VDiff(P, Centre)
  308.             Result = VLength(Radial) - Radius
  309.             if VDot(Sinister, Radial) < 0.0:
  310.               Result = -Result
  311.             return Result
  312.  
  313.           #---------------------------------------------------------------------
  314.  
  315.           def CTEFromLine(Pos, Heading, Line):
  316.             RLine = VDiff(Line[1], Line[0])
  317.             if VDot(Heading, RLine) >= 0:
  318.               RPos = VDiff(Pos, Line[0])
  319.             else:
  320.               RLine = VNeg(RLine)
  321.               RPos = VDiff(Pos, Line[1])
  322.             LineSinister = VNormalised(VPerp(RLine))
  323.             Result = VDot(RPos, LineSinister)
  324.             return Result
  325.  
  326.           #---------------------------------------------------------------------
  327.  
  328.           P = (self.x, self.y)
  329.           H = VPolToRect((1.0, self.orientation))
  330.  
  331.           StraightLength = 2.0 * radius
  332.  
  333.           CWest = (radius, radius)
  334.           CEast = (CWest[0] + StraightLength, CWest[1])
  335.           Middle = VLerp(CWest, CEast, 0.5)
  336.  
  337.           NorthStraight = (
  338.             (CWest[0], CWest[1] + radius),
  339.             (CEast[0], CEast[1] + radius)
  340.           )
  341.           SouthStraight = (
  342.             (CWest[0], CWest[1] - radius),
  343.             (CEast[0], CEast[1] - radius)
  344.           )
  345.  
  346.           if P[0] < CWest[0]:
  347.             cte = CTEFromCircle(P, H, CWest, radius)
  348.           elif P[0] > CEast[0]:
  349.             cte = CTEFromCircle(P, H, CEast, radius)
  350.           elif P[1] > Middle[1]:
  351.             cte = CTEFromLine(P, H, NorthStraight)
  352.           else:
  353.             cte = CTEFromLine(P, H, SouthStraight)
  354.  
  355.           return cte
  356.  
  357.   ############## ONLY ADD / MODIFY CODE ABOVE THIS LINE ####################
  358.  
  359.  
  360.  
  361.  
  362.   # ------------------------------------------------------------------------
  363.   #
  364.   # run - does a single control run.
  365.  
  366.  
  367.   def run(params, radius, printflag = False):
  368.       myrobot = robot()
  369.       myrobot.set(0.0, radius, pi / 2.0)
  370.       speed = 1.0 # motion distance is equal to speed (we assume time = 1)
  371.       err = 0.0
  372.       int_crosstrack_error = 0.0
  373.       N = 200
  374.       PathLog.append((myrobot.x, myrobot.y)) #<<< SVG-ONLY
  375.  
  376.       crosstrack_error = myrobot.cte(radius) # You need to define the cte function!
  377.  
  378.       for i in range(N*2):
  379.           diff_crosstrack_error = - crosstrack_error
  380.           crosstrack_error = myrobot.cte(radius)
  381.           diff_crosstrack_error += crosstrack_error
  382.           int_crosstrack_error += crosstrack_error
  383.           steer = - params[0] * crosstrack_error \
  384.                   - params[1] * diff_crosstrack_error \
  385.                   - params[2] * int_crosstrack_error
  386.           myrobot = myrobot.move(steer, speed)
  387.           if i >= N:
  388.               err += crosstrack_error ** 2
  389.           if printflag:
  390.               print myrobot
  391.           PathLog.append((myrobot.x, myrobot.y)) #<<< SVG-ONLY
  392.       return err / float(N)
  393.  
  394.   #-----------------------------------------------------------------------------
  395.  
  396.   return run(PIDParams, Radius, ShowProgress)
  397.  
  398.  
  399. #-------------------------------------------------------------------------------
  400.  
  401.  
  402. def RenderToSVG(Data):
  403.  
  404.   '''Return data rendered to an SVG file in a string.
  405.  
  406.  Grid is a dictionary with the keys:
  407.  
  408.    Title: This title is rendered and is embedded in the SVG header.
  409.    Grid: See SVGGrid().
  410.    Paths: A list of Shapes to be rendered in red-to-indigo rainbow colours.
  411.  
  412.  '''
  413.  
  414. #-------------------------------------------------------------------------------
  415.  
  416.  
  417. def RenderToSVG(Data):
  418.  
  419.   '''Return Data rendered to an SVG file in a string.
  420.  
  421.  Data is a dictionary with the keys:
  422.  
  423.    Title: This title is rendered and is embedded in the SVG header.
  424.    Grid: See SVGGrid().
  425.    Paths: A list of Shapes to be rendered in red-to-indigo rainbow colours.
  426.  
  427.  '''
  428.  
  429.   #-----------------------------------------------------------------------------
  430.  
  431.   # Shorthand
  432.  
  433.   A = Pt_Anchor
  434.   C = Pt_Control
  435.   B = (Pt_Break, None)
  436.  
  437.   #-----------------------------------------------------------------------------
  438.  
  439.   def Field(Name, Default):
  440.     return Data[Name] if Name in Data else Default
  441.  
  442.   #-----------------------------------------------------------------------------
  443.  
  444.   IT = tIndentTracker('  ')
  445.  
  446.   Title = Field('Title', '(Untitled)')
  447.  
  448.   Result = SVGStart(IT, Title)
  449.  
  450.   Result += IT('<defs>')
  451.   IT.StepIn()
  452.   Result += IT(
  453.     '<marker id="ArrowHead"',
  454.     '    viewBox="0 0 10 10" refX="0" refY="5"',
  455.     '    markerUnits="strokeWidth"',
  456.     '    markerWidth="8" markerHeight="6"',
  457.     '    orient="auto">'
  458.     '  <path d="M 0,0  L 10,5  L 0,10  z"/>',
  459.     '</marker>'
  460.   )
  461.   # More marker, symbol and gradient definitions can go here.
  462.   IT.StepOut()
  463.   Result += IT('</defs>')
  464.  
  465.   # Background
  466.  
  467.   Result += IT(
  468.     '<!-- Background -->',
  469.     '<rect x="0" y="0" width="28" height="19" stroke="none" fill="white"/>'
  470.   )
  471.  
  472.   # Outer group
  473.  
  474.   Result += IT('<!-- Outer group -->')
  475.   Result += SVGGroup(IT, {
  476.     'stroke': 'black',
  477.     'stroke-width': '0.025'
  478.   })
  479.  
  480.   # Plot with grid
  481.  
  482.   Result += IT('<!-- Grid -->')
  483.   Result += SVGGrid(IT, Data["Grid"])
  484.  
  485.   # Track
  486.  
  487.   Track = Field('Track', None)
  488.   if Track is not None:
  489.  
  490.     Result += IT('<!-- Track -->')
  491.     Result += SVGGroup(IT, {'fill': 'none'})
  492.  
  493.     Result += IT('<!-- Black edge -->')
  494.     Result += SVGPath(IT, Track, {
  495.       'stroke-width': '1.35',
  496.       'stroke': 'black'
  497.     })
  498.     Result += IT(u'<!-- Shoulder – White -->')
  499.     Result += SVGPath(IT, Track, {
  500.       'stroke-width': '1.3',
  501.       'stroke': 'white'
  502.     })
  503.     Result += IT(u'<!-- Shoulder – Red -->')
  504.     Result += SVGPath(IT, Track, {
  505.       'stroke-width': '1.3',
  506.       'stroke': '#d00',
  507.       'stroke-dasharray': '0.5 0.5',
  508.       'stroke-linecap': 'butt'
  509.     })
  510.     Result += IT('<!-- The track proper -->')
  511.     Result += SVGPath(IT, Track, {
  512.       'stroke-width': '1.0',
  513.       'stroke': '#ccc'
  514.     })
  515.     Result += IT('<!-- Centreline -->')
  516.     Result += SVGPath(IT, Track, {
  517.       'stroke-width': '0.1',
  518.       'stroke': 'white',
  519.       'stroke-dasharray': '1 1',
  520.       'stroke-linecap': 'butt'
  521.     })
  522.  
  523.     Result += SVGGroupEnd(IT)
  524.  
  525.   # Paths in rainbow colours
  526.  
  527.   Paths = Field('Paths', None)
  528.   if Paths is not None and len(Paths) > 0:
  529.  
  530.     NumPaths = len(Paths)
  531.  
  532.     Result += IT('<!-- Paths in rainbow colours -->')
  533.     for PathIx, Path in enumerate(Paths):
  534.       if NumPaths >= 2:
  535.         Progress = float(PathIx) / float(NumPaths - 1)
  536.       else:
  537.         Progress = 1.0
  538.       Opacity = 1.0 if Progress in [0.0, 1.0] else 0.60 + 0.00 * Progress
  539.       ColourStr = ProgressColourStr(Progress, Opacity)
  540.       Result += IT('<!-- Path %d, (%.1f%%) -->' % (PathIx, 100.0 * Progress))
  541.       Result += SVGPath(IT, Path, {"stroke": ColourStr})
  542.  
  543.   # End of plot
  544.  
  545.   Result += SVGGroupEnd(IT)
  546.  
  547.   # Title and legend
  548.  
  549.   Result += IT('<!-- Title background -->')
  550.   Result += IT(
  551.     '<rect x="0" y="0" width="28" height="1.1" stroke="none" fill="white"/>'
  552.   )
  553.  
  554.   Result += IT('<!-- Title group -->')
  555.   Result += SVGGroup(IT, {
  556.     'font-family': 'sans-serif',
  557.     'font-size': '0.36',
  558.     'font-weight': 'normal',
  559.     'fill': 'black',
  560.     'stroke': 'none'
  561.   })
  562.  
  563.   Result += IT('<!-- Title -->')
  564.   Result += SVGText(IT, (0.5, 0.82), Title, {
  565.     'font-size': '0.72',
  566.     'font-weight': 'bold'
  567.   })
  568.  
  569.   Result += IT('<!-- Legend line labels-->')
  570.   Result += SVGText(IT, (23.5, 0.82), 'Initial')
  571.   Result += SVGText(IT, (26.0, 0.82), 'Final')
  572.  
  573.   Result += IT('<!-- Legend lines -->')
  574.   Result += SVGGroup(IT, {
  575.     'fill': 'none',
  576.     'stroke-width': '0.1',
  577.     'stroke-linecap': 'round'
  578.   })
  579.  
  580.   Result += SVGPath(IT,
  581.     [(Pt_Anchor, (22.5, 0.7)), (Pt_Anchor, (23.3, 0.7))],
  582.     {'stroke': ProgressColourStr(0.0)}
  583.   )
  584.  
  585.   Result += SVGPath(IT,
  586.     [(Pt_Anchor, (25.0, 0.7)), (Pt_Anchor, (25.8, 0.7))],
  587.     {'stroke': ProgressColourStr(1.0)}
  588.   )
  589.  
  590.   Result += SVGGroupEnd(IT)
  591.  
  592.   # End of title group
  593.  
  594.   Result += SVGGroupEnd(IT)
  595.  
  596.   # End of outer group
  597.  
  598.   Result += SVGGroupEnd(IT)
  599.  
  600.   Result += SVGEnd(IT)
  601.  
  602.   return Result
  603.  
  604.  
  605. #-------------------------------------------------------------------------------
  606.  
  607.  
  608. def DoFunStuff(Data):
  609.  
  610.   '''Demonstrate a PID-controlled car following a racetrack.
  611.  
  612.  The output is written to the Data (a Python dictionary) to keep the
  613.  rendering details separate.
  614.  
  615.  '''
  616.  
  617.   #-----------------------------------------------------------------------------
  618.  
  619.   Data['Title'] = 'HW5-4: Racetrack'
  620.  
  621.   Radius = 25.0
  622.  
  623.   Grid = {
  624.     'CanvasMinima': (3.25, 4.25),
  625.     'CanvasMaxima': (24.75, 15.75),
  626.     'RangeMinima': (0, 0),
  627.     'RangeMaxima': (int(ceil(4 * Radius)), int(ceil(2 * Radius))),
  628.     'YIsUp': True,
  629.     'SquareAlignment': 'Corner',
  630.     'DrawGrid': True,
  631.     'GridLineAttributes': {
  632.       'stroke-width': '0.02', 'stroke': 'rgba(0, 192, 255, 0.5)'
  633.     },
  634.     'GeneralAttributes': {
  635.       'stroke-width': '0.05', 'stroke': 'red'
  636.     }
  637.   }
  638.  
  639.   r = Radius
  640.   Track = []
  641.   Track += PiecewiseArc((r, r), r, (1.5 * pi, 0.5 * pi), 8)
  642.   Track += PiecewiseArc((3.0 * r, r), r, (0.5 * pi, -0.5 * pi), 8)
  643.   Track.append(Track[0])
  644.  
  645.   Paths = []
  646.   PathLog = []
  647.  
  648.   PIDParams = [10.0, 15.0, 0.0]
  649.   Err = RunUnitCode(PIDParams, Radius, True, PathLog)
  650.   print '\nFinal parameters:', GFListStr(PIDParams)
  651.   print 'Error score:', Err
  652.  
  653.   Paths.append(ShapeFromVertices(PathLog, 1))
  654.  
  655.   Data['Grid'] = Grid
  656.   Data['Paths'] = Paths
  657.   Data['Track'] = Track
  658.  
  659.  
  660. #-------------------------------------------------------------------------------
  661. # Main
  662. #-------------------------------------------------------------------------------
  663.  
  664.  
  665. def Main():
  666.  
  667.   OutputFileName = 'output.svg'
  668.  
  669.   print 'Smoothing a loop with knots...'
  670.  
  671.   Data = {}
  672.   DoFunStuff(Data)
  673.  
  674.   print 'Rendering SVG...'
  675.   SVG = RenderToSVG(Data)
  676.   print 'Done.'
  677.  
  678.   print 'Saving SVG to "' + OutputFileName + '"...'
  679.   Save(SVG.encode('utf_8'), OutputFileName)
  680.   print 'Done.'
  681.  
  682.  
  683. #-------------------------------------------------------------------------------
  684. # Command line trigger
  685. #-------------------------------------------------------------------------------
  686.  
  687.  
  688. if __name__ == '__main__':
  689.   Main()
  690.  
  691.  
  692. #-------------------------------------------------------------------------------
  693. # End
  694. #-------------------------------------------------------------------------------
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement