2020-08-07 00:09:33 +00:00
|
|
|
|
2011-08-05 19:53:42 +00:00
|
|
|
/*
|
|
|
|
* TRANSLINE.cpp - base for a transmission line implementation
|
|
|
|
*
|
|
|
|
* Copyright (C) 2005 Stefan Jahn <stefan@lkcc.org>
|
2018-06-07 13:44:20 +00:00
|
|
|
* Modified for Kicad: 2018 jean-pierre.charras
|
2011-08-05 19:53:42 +00:00
|
|
|
*
|
|
|
|
* This program is free software; you can redistribute it and/or
|
|
|
|
* modify it under the terms of the GNU General Public License
|
|
|
|
* as published by the Free Software Foundation; either version 2
|
|
|
|
* 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 General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
* along with this package; see the file COPYING. If not, write to
|
|
|
|
* the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
|
|
|
|
* Boston, MA 02110-1301, USA.
|
|
|
|
*
|
|
|
|
*/
|
|
|
|
|
2016-05-28 16:46:29 +00:00
|
|
|
#include <cmath>
|
2011-09-18 15:11:09 +00:00
|
|
|
#include <limits>
|
2011-08-05 19:53:42 +00:00
|
|
|
|
2021-06-03 00:39:08 +00:00
|
|
|
#include <wx/colour.h>
|
|
|
|
#include <wx/settings.h>
|
|
|
|
|
2020-10-13 01:01:25 +00:00
|
|
|
#include "transline.h"
|
|
|
|
#include "units.h"
|
2013-02-11 00:41:49 +00:00
|
|
|
|
2011-09-18 15:11:09 +00:00
|
|
|
#ifndef INFINITY
|
|
|
|
#define INFINITY std::numeric_limits<double>::infinity()
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
|
|
#ifndef M_PI_2
|
2020-08-07 00:09:33 +00:00
|
|
|
#define M_PI_2 ( M_PI / 2 )
|
2011-09-18 15:11:09 +00:00
|
|
|
#endif
|
|
|
|
|
|
|
|
|
2011-08-05 19:53:42 +00:00
|
|
|
// Functions to Read/Write parameters in pcb_calculator main frame:
|
|
|
|
// They are wrapper to actual functions, so all transline functions do not
|
|
|
|
// depend on Graphic User Interface
|
|
|
|
void SetPropertyInDialog( enum PRMS_ID aPrmId, double value );
|
|
|
|
|
|
|
|
/* Puts the text into the given result line.
|
|
|
|
*/
|
|
|
|
void SetResultInDialog( int line, const char* text );
|
|
|
|
|
|
|
|
/* print aValue into the given result line.
|
|
|
|
*/
|
|
|
|
void SetResultInDialog( int aLineNumber, double aValue, const char* aText );
|
|
|
|
|
|
|
|
/* Returns a named property value. */
|
|
|
|
double GetPropertyInDialog( enum PRMS_ID aPrmId );
|
|
|
|
|
|
|
|
// Returns true if the param aPrmId is selected
|
|
|
|
// Has meaning only for params that have a radio button
|
2020-08-07 00:09:33 +00:00
|
|
|
bool IsSelectedInDialog( enum PRMS_ID aPrmId );
|
|
|
|
|
|
|
|
/** Function SetPropertyBgColorInDialog
|
|
|
|
* Set the background color of a parameter
|
|
|
|
* @param aPrmId = param id to set
|
|
|
|
* @param aCol = new color
|
|
|
|
*/
|
|
|
|
void SetPropertyBgColorInDialog( enum PRMS_ID aPrmId, const KIGFX::COLOR4D* aCol );
|
2011-08-05 19:53:42 +00:00
|
|
|
|
|
|
|
|
|
|
|
/* Constructor creates a transmission line instance. */
|
|
|
|
TRANSLINE::TRANSLINE()
|
|
|
|
{
|
2020-08-07 00:09:33 +00:00
|
|
|
m_parameters[MURC_PRM] = 1.0;
|
|
|
|
m_Name = nullptr;
|
2020-08-07 11:13:25 +00:00
|
|
|
ang_l = 0.0; // Electrical length in angle
|
2020-08-10 15:49:30 +00:00
|
|
|
len = 0.0; // length of line
|
|
|
|
er_eff = 1.0; // effective dielectric constant
|
2020-08-07 00:09:33 +00:00
|
|
|
Init();
|
2011-08-05 19:53:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Destructor destroys a transmission line instance. */
|
|
|
|
TRANSLINE::~TRANSLINE()
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2020-08-07 00:09:33 +00:00
|
|
|
void TRANSLINE::Init( void )
|
|
|
|
{
|
|
|
|
wxColour wxcol = wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW );
|
|
|
|
okCol = KIGFX::COLOR4D( wxcol );
|
|
|
|
okCol.r = wxcol.Red() / 255.0;
|
|
|
|
okCol.g = wxcol.Green() / 255.0;
|
|
|
|
okCol.b = wxcol.Blue() / 255.0;
|
|
|
|
int i;
|
|
|
|
// Initialize these variables mainly to avoid warnings from a static analyzer
|
|
|
|
for( i = 0; i < EXTRA_PRMS_COUNT; ++i )
|
|
|
|
{
|
|
|
|
m_parameters[i] = 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2011-08-05 19:53:42 +00:00
|
|
|
/* Sets a named property to the given value, access through the
|
|
|
|
* application.
|
|
|
|
*/
|
|
|
|
void TRANSLINE::setProperty( enum PRMS_ID aPrmId, double value )
|
|
|
|
{
|
|
|
|
SetPropertyInDialog( aPrmId, value );
|
|
|
|
}
|
|
|
|
|
2020-08-07 00:09:33 +00:00
|
|
|
|
2011-08-05 19:53:42 +00:00
|
|
|
/*
|
|
|
|
*Returns true if the param aPrmId is selected
|
|
|
|
* Has meaning only for params that have a radio button
|
|
|
|
*/
|
|
|
|
bool TRANSLINE::isSelected( enum PRMS_ID aPrmId )
|
|
|
|
{
|
|
|
|
return IsSelectedInDialog( aPrmId );
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Puts the text into the given result line.
|
|
|
|
*/
|
|
|
|
void TRANSLINE::setResult( int line, const char* text )
|
|
|
|
{
|
|
|
|
SetResultInDialog( line, text );
|
|
|
|
}
|
|
|
|
void TRANSLINE::setResult( int line, double value, const char* text )
|
|
|
|
{
|
|
|
|
SetResultInDialog( line, value, text );
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Returns a property value. */
|
|
|
|
double TRANSLINE::getProperty( enum PRMS_ID aPrmId )
|
|
|
|
{
|
|
|
|
return GetPropertyInDialog( aPrmId );
|
|
|
|
}
|
|
|
|
|
2020-08-07 00:09:33 +00:00
|
|
|
/** @function getProperties
|
2020-08-07 11:13:25 +00:00
|
|
|
*
|
2020-08-07 00:09:33 +00:00
|
|
|
* Get all properties from the UI. Computes some extra ones.
|
|
|
|
**/
|
|
|
|
void TRANSLINE::getProperties( void )
|
|
|
|
{
|
|
|
|
int i;
|
|
|
|
for( i = 0; i < DUMMY_PRM; ++i )
|
|
|
|
{
|
|
|
|
m_parameters[i] = getProperty( (PRMS_ID) i );
|
|
|
|
setErrorLevel( (PRMS_ID) i, TRANSLINE_OK );
|
|
|
|
}
|
|
|
|
m_parameters[SIGMA_PRM] = 1.0 / getProperty( RHO_PRM );
|
|
|
|
m_parameters[EPSILON_EFF_PRM] = 1.0;
|
|
|
|
m_parameters[SKIN_DEPTH_PRM] = skin_depth();
|
|
|
|
}
|
|
|
|
/** @function checkProperties
|
2020-08-07 11:13:25 +00:00
|
|
|
*
|
2020-08-07 00:09:33 +00:00
|
|
|
* Checks the input parameters (ie: negative length).
|
|
|
|
* Does not check for incompatibility between values as this depends on the line shape.
|
|
|
|
**/
|
|
|
|
void TRANSLINE::checkProperties( void )
|
|
|
|
{
|
2021-06-09 19:32:58 +00:00
|
|
|
// Do not check for values that are results of analyzing / synthesizing
|
|
|
|
// Do not check for transline specific incompatibilities ( like " conductor height should be lesser than dielectric height")
|
2020-08-07 00:09:33 +00:00
|
|
|
if( !std::isfinite( m_parameters[EPSILONR_PRM] ) || m_parameters[EPSILONR_PRM] <= 0 )
|
|
|
|
setErrorLevel( EPSILONR_PRM, TRANSLINE_WARNING );
|
|
|
|
|
|
|
|
if( !std::isfinite( m_parameters[TAND_PRM] ) || m_parameters[TAND_PRM] < 0 )
|
|
|
|
setErrorLevel( TAND_PRM, TRANSLINE_WARNING );
|
|
|
|
|
|
|
|
if( !std::isfinite( m_parameters[RHO_PRM] ) || m_parameters[RHO_PRM] < 0 )
|
|
|
|
setErrorLevel( RHO_PRM, TRANSLINE_WARNING );
|
|
|
|
|
|
|
|
if( !std::isfinite( m_parameters[H_PRM] ) || m_parameters[H_PRM] < 0 )
|
|
|
|
setErrorLevel( H_PRM, TRANSLINE_WARNING );
|
|
|
|
|
|
|
|
if( !std::isfinite( m_parameters[TWISTEDPAIR_TWIST_PRM] )
|
|
|
|
|| m_parameters[TWISTEDPAIR_TWIST_PRM] < 0 )
|
|
|
|
setErrorLevel( TWISTEDPAIR_TWIST_PRM, TRANSLINE_WARNING );
|
|
|
|
|
|
|
|
if( !std::isfinite( m_parameters[STRIPLINE_A_PRM] ) || m_parameters[STRIPLINE_A_PRM] <= 0 )
|
|
|
|
setErrorLevel( STRIPLINE_A_PRM, TRANSLINE_WARNING );
|
|
|
|
|
|
|
|
if( !std::isfinite( m_parameters[H_T_PRM] ) || m_parameters[H_T_PRM] <= 0 )
|
|
|
|
setErrorLevel( H_T_PRM, TRANSLINE_WARNING );
|
|
|
|
|
|
|
|
// How can we check ROUGH_PRM ?
|
|
|
|
|
|
|
|
if( !std::isfinite( m_parameters[MUR_PRM] ) || m_parameters[MUR_PRM] < 0 )
|
|
|
|
setErrorLevel( MUR_PRM, TRANSLINE_WARNING );
|
|
|
|
|
|
|
|
if( !std::isfinite( m_parameters[TWISTEDPAIR_EPSILONR_ENV_PRM] )
|
|
|
|
|| m_parameters[TWISTEDPAIR_EPSILONR_ENV_PRM] <= 0 )
|
|
|
|
setErrorLevel( TWISTEDPAIR_EPSILONR_ENV_PRM, TRANSLINE_WARNING );
|
|
|
|
|
|
|
|
if( !std::isfinite( m_parameters[MURC_PRM] ) || m_parameters[MURC_PRM] < 0 )
|
|
|
|
setErrorLevel( MURC_PRM, TRANSLINE_WARNING );
|
|
|
|
|
|
|
|
if( !std::isfinite( m_parameters[FREQUENCY_PRM] ) || m_parameters[FREQUENCY_PRM] <= 0 )
|
|
|
|
setErrorLevel( FREQUENCY_PRM, TRANSLINE_WARNING );
|
|
|
|
}
|
|
|
|
|
|
|
|
void TRANSLINE::analyze()
|
|
|
|
{
|
|
|
|
getProperties();
|
|
|
|
checkProperties();
|
|
|
|
calcAnalyze();
|
|
|
|
showAnalyze();
|
|
|
|
show_results();
|
|
|
|
}
|
|
|
|
|
|
|
|
void TRANSLINE::synthesize()
|
|
|
|
{
|
|
|
|
getProperties();
|
|
|
|
checkProperties();
|
|
|
|
calcSynthesize();
|
|
|
|
showSynthesize();
|
|
|
|
show_results();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @function skin_depth
|
|
|
|
* calculate skin depth
|
2020-08-07 11:13:25 +00:00
|
|
|
*
|
2020-08-07 00:09:33 +00:00
|
|
|
* \f$ \frac{1}{\sqrt{ \pi \cdot f \cdot \mu \cdot \sigma }} \f$
|
2011-08-05 19:53:42 +00:00
|
|
|
*/
|
2019-12-05 14:03:15 +00:00
|
|
|
#include <cstdio>
|
2011-08-05 19:53:42 +00:00
|
|
|
double TRANSLINE::skin_depth()
|
|
|
|
{
|
|
|
|
double depth;
|
2020-08-07 00:09:33 +00:00
|
|
|
depth = 1.0
|
|
|
|
/ sqrt( M_PI * m_parameters[FREQUENCY_PRM] * m_parameters[MURC_PRM] * MU0
|
|
|
|
* m_parameters[SIGMA_PRM] );
|
2011-08-05 19:53:42 +00:00
|
|
|
return depth;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* *****************************************************************
|
|
|
|
********** **********
|
|
|
|
********** mathematical functions **********
|
|
|
|
********** **********
|
|
|
|
***************************************************************** */
|
|
|
|
|
|
|
|
#define NR_EPSI 2.2204460492503131e-16
|
|
|
|
|
|
|
|
/* The function computes the complete elliptic integral of first kind
|
|
|
|
* K() and the second kind E() using the arithmetic-geometric mean
|
2016-04-18 18:15:44 +00:00
|
|
|
* algorithm (AGM) by Abramowitz and Stegun.
|
|
|
|
*/
|
2011-08-05 19:53:42 +00:00
|
|
|
void TRANSLINE::ellipke( double arg, double& k, double& e )
|
|
|
|
{
|
|
|
|
int iMax = 16;
|
|
|
|
|
|
|
|
if( arg == 1.0 )
|
|
|
|
{
|
|
|
|
k = INFINITY; // infinite
|
|
|
|
e = 0;
|
|
|
|
}
|
2013-08-18 15:49:04 +00:00
|
|
|
else if( std::isinf( arg ) && arg < 0 )
|
2011-08-05 19:53:42 +00:00
|
|
|
{
|
|
|
|
k = 0;
|
|
|
|
e = INFINITY; // infinite
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2016-04-18 18:15:44 +00:00
|
|
|
double a, b, c, fr, s, fk = 1, fe = 1, t, da = arg;
|
2011-08-05 19:53:42 +00:00
|
|
|
int i;
|
|
|
|
if( arg < 0 )
|
|
|
|
{
|
|
|
|
fk = 1 / sqrt( 1 - arg );
|
|
|
|
fe = sqrt( 1 - arg );
|
2020-08-07 00:09:33 +00:00
|
|
|
da = -arg / ( 1 - arg );
|
2011-08-05 19:53:42 +00:00
|
|
|
}
|
2020-08-07 00:09:33 +00:00
|
|
|
a = 1;
|
|
|
|
b = sqrt( 1 - da );
|
|
|
|
c = sqrt( da );
|
2016-04-18 18:15:44 +00:00
|
|
|
fr = 0.5;
|
2020-08-07 00:09:33 +00:00
|
|
|
s = fr * c * c;
|
2011-08-05 19:53:42 +00:00
|
|
|
for( i = 0; i < iMax; i++ )
|
|
|
|
{
|
2020-08-07 00:09:33 +00:00
|
|
|
t = ( a + b ) / 2;
|
|
|
|
c = ( a - b ) / 2;
|
|
|
|
b = sqrt( a * b );
|
|
|
|
a = t;
|
2016-04-18 18:15:44 +00:00
|
|
|
fr *= 2;
|
|
|
|
s += fr * c * c;
|
2011-08-05 19:53:42 +00:00
|
|
|
if( c / a < NR_EPSI )
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if( i >= iMax )
|
|
|
|
{
|
2020-08-07 00:09:33 +00:00
|
|
|
k = 0;
|
|
|
|
e = 0;
|
2011-08-05 19:53:42 +00:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
k = M_PI_2 / a;
|
2020-08-07 00:09:33 +00:00
|
|
|
e = M_PI_2 * ( 1 - s ) / a;
|
2011-08-05 19:53:42 +00:00
|
|
|
if( arg < 0 )
|
|
|
|
{
|
|
|
|
k *= fk;
|
|
|
|
e *= fe;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* We need to know only K(k), and if possible KISS. */
|
|
|
|
double TRANSLINE::ellipk( double k )
|
|
|
|
{
|
|
|
|
double r, lost;
|
|
|
|
|
|
|
|
ellipke( k, r, lost );
|
|
|
|
return r;
|
|
|
|
}
|
2020-08-07 00:09:33 +00:00
|
|
|
|
|
|
|
#define MAX_ERROR 0.000001
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @function minimizeZ0Error1D
|
2020-08-07 11:13:25 +00:00
|
|
|
*
|
|
|
|
* Tries to find a parameter that minimizes the error ( on Z0 ).
|
2020-08-07 00:09:33 +00:00
|
|
|
* This function only works with a single parameter.
|
|
|
|
* Calls @ref calcAnalyze several times until the error is acceptable.
|
|
|
|
* While the error is unnacceptable, changes slightly the parameter.
|
2020-08-07 11:13:25 +00:00
|
|
|
*
|
2020-08-07 00:09:33 +00:00
|
|
|
* This function does not change Z0 / Angl_L.
|
2020-08-07 11:13:25 +00:00
|
|
|
*
|
2020-08-07 00:09:33 +00:00
|
|
|
* @param avar Parameter to synthesize
|
|
|
|
* @return 'true' if error < MAX_ERROR, else 'false'
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
bool TRANSLINE::minimizeZ0Error1D( double* aVar )
|
|
|
|
{
|
|
|
|
double Z0_dest, Z0_current, Z0_result, angl_l_dest, increment, slope, error;
|
|
|
|
int iteration;
|
|
|
|
|
|
|
|
if( !std::isfinite( m_parameters[Z0_PRM] ) )
|
|
|
|
{
|
|
|
|
*aVar = NAN;
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if( ( !std::isfinite( *aVar ) ) || ( *aVar == 0 ) )
|
|
|
|
{
|
|
|
|
*aVar = 0.001;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* required value of Z0 */
|
|
|
|
Z0_dest = m_parameters[Z0_PRM];
|
|
|
|
|
|
|
|
/* required value of angl_l */
|
|
|
|
angl_l_dest = m_parameters[ANG_L_PRM];
|
|
|
|
|
|
|
|
/* Newton's method */
|
|
|
|
iteration = 0;
|
|
|
|
|
|
|
|
/* compute parameters */
|
|
|
|
calcAnalyze();
|
|
|
|
Z0_current = m_parameters[Z0_PRM];
|
|
|
|
|
|
|
|
error = fabs( Z0_dest - Z0_current );
|
|
|
|
|
|
|
|
while( error > MAX_ERROR )
|
|
|
|
{
|
|
|
|
iteration++;
|
|
|
|
increment = *aVar / 100.0;
|
|
|
|
*aVar += increment;
|
|
|
|
/* compute parameters */
|
|
|
|
calcAnalyze();
|
|
|
|
Z0_result = m_parameters[Z0_PRM];
|
|
|
|
/* f(w(n)) = Z0 - Z0(w(n)) */
|
|
|
|
/* f'(w(n)) = -f'(Z0(w(n))) */
|
|
|
|
/* f'(Z0(w(n))) = (Z0(w(n)) - Z0(w(n+delw))/delw */
|
|
|
|
/* w(n+1) = w(n) - f(w(n))/f'(w(n)) */
|
|
|
|
slope = ( Z0_result - Z0_current ) / increment;
|
|
|
|
slope = ( Z0_dest - Z0_current ) / slope - increment;
|
|
|
|
*aVar += slope;
|
|
|
|
if( *aVar <= 0.0 )
|
|
|
|
*aVar = increment;
|
|
|
|
/* find new error */
|
|
|
|
/* compute parameters */
|
|
|
|
calcAnalyze();
|
|
|
|
Z0_current = m_parameters[Z0_PRM];
|
|
|
|
error = fabs( Z0_dest - Z0_current );
|
|
|
|
|
|
|
|
if( iteration > 100 )
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Compute one last time, but with correct length */
|
|
|
|
m_parameters[Z0_PRM] = Z0_dest;
|
|
|
|
m_parameters[ANG_L_PRM] = angl_l_dest;
|
|
|
|
m_parameters[PHYS_LEN_PRM] = C0 / m_parameters[FREQUENCY_PRM]
|
|
|
|
/ sqrt( m_parameters[EPSILON_EFF_PRM] ) * m_parameters[ANG_L_PRM]
|
|
|
|
/ 2.0 / M_PI; /* in m */
|
|
|
|
calcAnalyze();
|
|
|
|
|
|
|
|
/* Restore parameters */
|
|
|
|
m_parameters[Z0_PRM] = Z0_dest;
|
|
|
|
m_parameters[ANG_L_PRM] = angl_l_dest;
|
|
|
|
m_parameters[PHYS_LEN_PRM] = C0 / m_parameters[FREQUENCY_PRM]
|
|
|
|
/ sqrt( m_parameters[EPSILON_EFF_PRM] ) * m_parameters[ANG_L_PRM]
|
|
|
|
/ 2.0 / M_PI; /* in m */
|
|
|
|
return error <= MAX_ERROR;
|
|
|
|
}
|
|
|
|
/**
|
|
|
|
* @function setErrorLevel
|
2020-08-07 11:13:25 +00:00
|
|
|
*
|
2020-08-07 00:09:33 +00:00
|
|
|
* set an error / warning level for a given parameter.
|
2020-08-07 11:13:25 +00:00
|
|
|
*
|
2020-08-07 00:09:33 +00:00
|
|
|
* @see TRANSLINE_OK
|
|
|
|
* @see TRANSLINE_WARNING
|
|
|
|
* @see TRANSLINE_ERROR
|
2020-08-07 11:13:25 +00:00
|
|
|
*
|
2020-08-07 00:09:33 +00:00
|
|
|
* @param aP parameter
|
|
|
|
* @param aErrorLevel Error level
|
|
|
|
*/
|
|
|
|
void TRANSLINE::setErrorLevel( PRMS_ID aP, char aErrorLevel )
|
|
|
|
{
|
|
|
|
switch( aErrorLevel )
|
|
|
|
{
|
|
|
|
case( TRANSLINE_WARNING ):
|
|
|
|
SetPropertyBgColorInDialog( aP, &warnCol );
|
|
|
|
break;
|
|
|
|
case( TRANSLINE_ERROR ):
|
|
|
|
SetPropertyBgColorInDialog( aP, &errCol );
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
SetPropertyBgColorInDialog( aP, &okCol );
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|