[iPhone] - 시작하세요! 아이폰 3 프로그래밍 - Part 15. 야호! 가속도센서!

iPhone/[위키북스]시작하세요! 아이폰3 프로그래밍 2010. 9. 15. 20:53

* 야호! 가속도센서!
- 아이폰과 아이팟 터치의 가장 큰 특징 중 하나는 가속도 센서(Accelerometer)를 내장하고 있다는 것이며, 이 작은 센서를 사용하면 아이폰이 정지 상태인지 이동 중인지를 판단할 수 있다.

* 가속도 센서의 물리법칙
- 가속도 센서는 특정 방향으로 작용하는 관성력을 감지하여 가속도와 중력을 측정한다.
- 가속도 센서는 x,y,z의 세방향에 대한 가속도의 크기를 측정할 수 있기 때문에 3차원 공간에서의 움직임이나 중력을 감지하는 것이 가능하다. 결국, 가속도 센서는 x,y축을 기준으로 아이폰이 자동회전 후 정지 상태인지 판단할 수 있을 뿐만 아니라 아이폰이 탁자 위에 놓여져 있는 상태라면 z축을 기준으로 액정 화면이 위와 아래 중 어느 쪽을 향하는지까지도 확인할수 있다.
- 가속도 센서는 중력이 작용하는 범위 안에서 측정된 가속도 값을 제공한다. 그래서 가속도 센서의 반환 값이 1.0이면, 특정 한 방향으로 1g(1g는 자연상태의 중력 가속도이며, 9.8m/s2)의 힘이 작용하고 있음을 나타낸다.
- 만일 아이폰이 움직임 없이 가만히 멈춰 있다면, 지구가 끌어 당기는 방향으로 1g가량의 힘이 작용하고 있는 것이다. 아이폰이 세로 방향으로 세워져 있다면 가속도 센서는 y축의 방향으로 1g의 힘이 작용하고 있다고 알려줄 것이며, 아이폰이 비스듬한 모양으로 정지해 있다면 아이폰의 기울기에 따라 1g의 힘은 각각의 방향으로 분산되어 작용하고 있을 것이다. 예를 들어, 아이폰이 45º의 각도로 정지해 있다면, 1g의 힘은 아이폰을 기준으로 양쪽 방향으로 균등하게 분산되어 작용하고 있을 것이다.
- 한 가지 중요한 점은 가속도 센서는 힘이 위쪽 방향으로 작용할 때 y좌표계를 사용한다는 것이다. 이것은 쿼츠 2D의 y좌표계와는 대칭되기 때문에 쿼츠 2D를 사용하여 가속도를 반영할 때는 y좌표계를 변환하는 작업이 필요하다. 반면, OpenGL ES을 사용하여 가속도 값을 반영할 때는 좌표계를 변환하여 사용할 필요가 없다.


* 가속도 센서 사용하기
- UIAccelerometer클래스는 싱글턴으로 설계되었다. 따라서, 이 클래스의 레퍼런스를 생성하려면 아래와 같이 sharedAccelerometer메서드를 호출해야한다.
UIAccelerometer *accelerometer = [UIAccelerometer sharedAccelerometer];

- 가속도 센서로부터 데이터를 가져오는 방법은 코어 로케이션을 사용하는것과 비슷하다.
- 먼저 UIAccelerometerDelegate 프로토콜을 따르는 클래스를 생성하고, 가속도 센서로부터 데이터를 전달 받을 메서드를 구현한다. 그런다음 클래스의 인스턴스를 생성하여 인스턴스를 가속도 센서의 델리게이트로 할당한다.
- 델리게이트로 할당할 때 업데이트 주기를 초 단위로 명시할 필요가 있다. 비록 업데이트 횟수나 업데이트간격이 정확하게 지켜진다는 보장은 없지만 가속도 센서는 초당 100회에 이르는 폴링이 가능하다. 아래와 같은 방법을 사용하면 델리게이트를 할당하고 폴링 주기를 초당 60회로 설정할수 있다.
accelerometer.delegate = self;
accelerometer.updateInterval = 1.0f/60.0f;

- 델리게이트 객체를 할당하고 폴링 주기를 설정하고 나면, accelerometer:didAccelerate:메서드를 구현하여 가속도 센서가 델리게이트를 업데이트 할 수 있도록 만들어야 한다.
- accelerometer:didAccelerate:메서드는 2개의 인자를 가지고 있다. 첫번째 인자는 UIAccelerometer인스턴스 레퍼런스이며, 두번째 인자는 가속도 센서로부터 받은 실제 데이터를 가지고 있는 UIAcceleration객체의 인스턴스이다.

* UIAcceleration
- UIAcceleration는 x,y,z라는 3개의 프로퍼티를 가지고 있으며, 각각의 프로퍼티는 부동 소수점 값을 저장하고 있다. 만일 특정 프로퍼티의 값이 0이면, 가속도 센서가 프로퍼티가 나타내는 방향으로 어떠한 움직임도 감지하지 못했다는 것을 의미한다. 반면에 양(+)이나 음(-)의 값을 가진다면 그 방향으로 가속도가 발생했음을 의미한다. 예를 들어 y가 음수이면 아래 방향으로 당기는 힘이 작용한 것이고, 아마도 아이폰은 똑바로 세워진 채로 정지해 있을 것이다. 반대로 y가 양수이면 아이폰의 위쪽 방향으로 힘이 작용한 것이며, 아이폰은 거꾸로 세워져 있거나 위쪽 방향으로 이동하고 있음을 나타내는 것이다.

* accelerometer:didAccelerate:메서드 구현하기
- 가속도 센서의 정보를 받을 델리게이트 클래스는 accelerometer:didAccelerate:메서드가 구현되어 있어야 한다.
- UILabel을 사용하여 가속도 값을 표시하려면 accelerometer:didAccelerate:메서드에 아래와 같은 코드를 구현해야 한다.
-(void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration
{
  NSString *newText = [[NSString alloc] initWithFormat:@"x: %g\t y:%g\t z:%g", acceleration.x, acceleration.y, acceleration.z];
  label.text = newText;
  [newText release];
}

- 앞에서 설정한 updateInterval값에 따라 이 메서드의 호출 빈도가 결정된다.

* 흔들기 인식하기
- 체스처와 마찬가지로 흔들기 인식은 애플리케이션에 사용자의 명령을 전달하는 수단으로 사용될 것이다.
- 흔들기 인식하는 것은 어려지 않다. 흔들기를 인식하기 위해서는 세 방향으로 작용하는 가속도 중 하나가 기준이 되는 값 이상인지를 확인하면된다. 특정 방향으로 1.3gs 가량의 가속도가 발생하는 것은 흔한 일이지만, 인의적인 힘을 가하지 않고서는 발생시킬 수 없는 비교적 큰 힘이다.
- 흔들기 인식을 구현할 때 가변운 흔들기를 인식하고 가속도가 1.5보다 큰 값인지 확인하고, 강한 흔들기를 인식하고 싶다면 가속도가 2.0보다 큰 값인지 확인하면된다. 가속도의 크기를 비교하는 방법은 다음과 같다.
-(void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration
{
  if(fabsf(acceleration.x) > 2.0 || fabsf(acceleration.y) > 2.0 || fabsf(acceleration.z) > 2.0)
  {
    // Do Something here....
  }
}

- 다음과 같이 일정 횟수 이상의 흔들기가 발생하였는지 확인하는 코드를 추가한다면, 좀 더 정교한 흔들기 인식을 구현할 수 있을 것이다.
-(void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration
{
  static NSInteger shakeCount = 0;
  static NSDate *shakeStart;

  NSDate *now = [[NSDate alloc] init];
  NSDate *checkDate = [[NSDate alloc] initWithTimeInterval:1.5f sinceDate:shakeStart];

  if([now compare:checkDate] == NSOrderedDescending || shakeCount == nil)
  {
    shakeCount = 0;
    [shakeStart release];
    shakeStart = [[NSDate alloc] init];
  }
  [now release];
  [checkDate release];

  if(fabsf(acceleration.x) > 2.0 || fabsf(acceleration.y) > 2.0 || fabsf(acceleration.z) > 2.0)
  {
    shakeCount++;
    if(shakeCount > 4)
    {
      shakeCount = 0;
      [shakeStart release];
      shakeStart = [[NSDate alloc] init];
    }
  }
}

* 가속도 센서를 통한 방향 제어
- 가속도 센서를 컨트롤러로 사용할 때 가장 어려운 점은, 델리게이트 메서드가 설정된 시간만큰 일정한 간격으로 호출되지 않는다는 점이다. 만일 가속도 센서가 델리게이트를 1초에 60회 업데이트하도록 설정하면, 업데이트가 1초에 60번 이하로 발생하는 것뿐이지, 반드시 1초 간격으로 한번씩 업데이트가 일어나는 것은 아니다.
- 안타깝게도 시뮬레이터에는 가속도 센서가 없어서 예제 애플리케이션을 테스트할 수 없다.

* 흔들어서 깨트리기
- 사실 아이폰의 화면을 실제로 깨트릴 것은 아니고, 흔들기를 인식하여 아이폰의 화면이 깨진 것과 같은 시각과 청각효과를 동시에 보여줄 것이다.

* 화면 깨뜨리기 코드
- Xcode에서 뷰 기반의 템플릿을 사용하여 새 프로젝트를 하나 생성하고, ShakeAndBreak라고 입력한다.
- 샘플 소스에서 home.png, homebroken.png. glass.wav파일을 찾아서 Resources폴더에 카피한다.
- Resources 폴더를 펼쳐 ShakeAndBreak-Info.plist파일을 클릭한다. 애플리케이션에서 상태 표시줄(status Bar)를 사용하지 않으려면, 프로퍼티 리스트에 새 프로퍼티를 추가해야 한다. Information Property list를 클릭하고 맨 끝에 보이는 버튼을 마우스로 클릭하여 하위 항목을 하나 추가한다. 새로 생선된 프로퍼티의 Key를 UIStatusBarHidden으로 변경하고 체크 박스를 체크한다.
- 내 환경은 sdk 4를 사용해서 그런지 위에 항목이 없다 그래서. Status bar is initially hidden를 넣어주고 체크 박스를 체크했다.
- Icon file 프로퍼티에 icon.png라고 입력한다.

#### ShakeAndBreakViewController.h ####
#import 
#import 

#define kAccelerationThreshold 2.2
#define kUpdateInterval (1.0f/10.0f)

@interface ShakeAndBreakViewController : UIViewController 
{
	// 화면에 나타낼 이미지를 변경할 수 있도록 이미지 뷰를 가리키는 아웃렛
	UIImageView *imageView;
	// 화면의 초기화 상태를 저장하기 위한 Boolean 변수
	BOOL brokenScreenShowing;
	// 사운드 파일을 참조하기 위한 사운드 ID
	SystemSoundID soundID;
	// 메모리에 로딩한 이미지를 가리키기위한 UIImage
	UIImage *fixed;
	UIImage *broken;
}

@property (nonatomic, retain) IBOutlet UIImageView *imageView;
@property (nonatomic, retain) UIImage *fixed;
@property (nonatomic, retain) UIImage *broken;

@end

#### ShakeAndBreakViewController.m ####
#import "ShakeAndBreakViewController.h"

@implementation ShakeAndBreakViewController
@synthesize imageView;
@synthesize fixed;
@synthesize broken;

-(void)viewDidLoad
{
	// 가속도 센서의 인스턴스를 생성
	UIAccelerometer *accel = [UIAccelerometer sharedAccelerometer];
	// 자기 자신을 가속도 센서의 델리게이트로 설정
	accel.delegate = self;
	// 업데이트 빈도를 설정
	accel.updateInterval = kUpdateInterval;
	
	// glass.wav 파일을 로딩하고, 할당된 식별자를 soundID 인스턴스 변수에 저장
	NSString *path = [[NSBundle mainBundle] pathForResource:@"glass" ofType:@"wav"];
	AudioServicesCreateSystemSoundID((CFURLRef)[NSURL fileURLWithPath:path],&soundID);
	
	// 이미지 파일을 로딩
	self.fixed = [UIImage imageNamed:@"home.png"];
	self.broken = [UIImage imageNamed:@"homebroken.png"];
	
	// 유리가 깨지지 않은 그림을 화면에 보여주기 위해 imageView를 설정하고
	// 화면을 재설정 하지 않기 위해 brokenScreenShowing에 NO를 할당
	imageView.image = fixed;
	brokenScreenShowing = NO;
	
	// 아래는 Motion Handling
	[self.view becomeFirstResponder];
}

// 아래는 Motion Handling
- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    [self becomeFirstResponder];
}

// 아래는 Motion Handling
- (BOOL) canBecomeFirstResponder {
    return YES;
}

#pragma mark -
-(void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration
{
	// brokenScreenShowing이 NO라면, 화면에는 이미 유리가 깨진 이미지가
	// 보여지고 있으므로 메서드는 아무 일도 하지 않는다.
	if(!brokenScreenShowing)
	{
		// 세 방향중 어느 한 방향이라도 조건에 만족하는 힘이 작용하였다면, 
		// 이미지 뷰에 유리가 깨진 이미지가 그려지고 효과음이 재생될 것이다.
		// 그러고 나서 화면이 초기화되기 전에 이미지와 사운드 파일이 한번 더 재생되는 것을 막기 위해 
		// brokenScreenShowing을 YES로 설정
		if(acceleration.x > kAccelerationThreshold || acceleration.y > kAccelerationThreshold
		   || acceleration.z > kAccelerationThreshold)
		{
			imageView.image  = broken;
			AudioServicesPlaySystemSound(soundID);
			brokenScreenShowing = YES;
		}
	}
}

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
	imageView.image = fixed;
	brokenScreenShowing = NO;
}

#pragma mark Motion Handling
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event {
    imageView.image = broken;
    AudioServicesPlaySystemSound (soundID);
    brokenScreenShowing = YES;    
    [super motionEnded:motion withEvent:event];
}

- (void)didReceiveMemoryWarning {
	[super didReceiveMemoryWarning];
}

- (void)viewDidUnload {
	self.imageView = nil;
	self.fixed = nil;
	self.broken = nil;
	[super viewDidUnload];
}

- (void)dealloc {
	[imageView dealloc];
	[fixed dealloc];
	[broken dealloc];
    [super dealloc];
}

@end

- 마지막으로 AudioToolbox.framework를 추가하여 사운드 파일을 재생할 수 있게 만든다.

* 구슬 굴리기 프로그램
- 애니메이션 처리를 위해서 쿼츠 2D를 사용할 것이다.
- Xcode에서 뷰 기반의 템플릿을 사용하여 새 프로젝트를 만들고 Ball이라고 입력한다. 예제 소스 폴더 안에 있는 15 Ball 폴더를 열면 ball.png라는 이미지 파일을 찾을 수 있다. 이 파일을 Resources폴더에 추가한다.
- Classes폴더를 클릭하고 New File로 Cocoa Touch Class카테고리의 Objective-C class와 Subclass of 팝업 메뉴의 UIView를 선택한다. 그런 다음 파일 이름을 BallView.m로 변경한다. BallView.m파일을 생성하면 헤더 파일도 자동으로 생성되므로, 확인한다.
- BallViewController.xib를 더블클릭하면, 인터페이스 빌더가 실행되고 파일이 열릴 것이다. 뷰 아이콘을 클릭하고 아이덴터티 인스펙터 창을 열어서, 뷰 클래스를 UIView에서 VallView로 변경한다. View항목의 Backgroup를 검정색으로 변경한다. 그 다음 File's Owner아이콘에서 Ball View으로 컨트롤-드래그하고, 컨트롤러와 뷰를 연결하기 위해 view아웃렛을 선택한다.

#### BallViewController.h ####
#import 
#define kUpdateInterVal (1.0f/60.0f)

@interface BallViewController : UIViewController 
{

}

@end

#### BallViewController.m ####
#import "BallViewController.h"
#import "BallView.h"

@implementation BallViewController

-(void)viewDidLoad
{
	UIAccelerometer *accelerometer = [UIAccelerometer sharedAccelerometer];
	accelerometer.delegate = self;
	accelerometer.updateInterval = kUpdateInterVal;
	[super viewDidLoad];	
}

#pragma mark -
-(void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration
{
	[(BallView *)self.view setAcceleration:acceleration];
	[(BallView *)self.view draw];
}


- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
}

- (void)viewDidUnload {
}

- (void)dealloc {
    [super dealloc];
}

@end

#### BallView.h ####
#import 

#define kVelocityMultiplier 500

@interface BallView : UIView 
{
	// 화면을 이동할 구슬 이미지를 참조하는데 사용
	UIImage *image;
	// 현재의 위치를 저장한다.
	CGPoint currentPoint;
	// 구슬의 이전 위치와 현재 위치를 둘러싸는 업데이트 영역을 정의하기 위해 마지막 위치를 저장한다.
	CGPoint previousPoint;
	// 컨트롤러 클래스를 통해 넘겨받은 인자를 이 프로퍼티에 저장한다.
	UIAcceleration *acceleration;
	// 구슬의 현재 속도를 저장히기 위해 변수 2개를 선언한다. 
	// 현재 속도 = 이전 속도 + 가속된 속도
	CGFloat ballXVelocity;
	CGFloat ballYVelocity;
}

@property (nonatomic, retain) UIImage *image;
@property CGPoint currentPoint;
@property CGPoint previousPoint;
@property (nonatomic, retain) UIAcceleration *acceleration;
@property CGFloat ballXVelocity;
@property CGFloat ballYVelocity;
-(void)draw;
@end

#### BallView.m ####
#import "BallView.h"

@implementation BallView
@synthesize image;
@synthesize currentPoint;
@synthesize previousPoint;
@synthesize acceleration;
@synthesize ballXVelocity;
@synthesize ballYVelocity;

// Nib로부터 뷰를 로딩할때, initWithCoder:메서드를 재 호출하였다.
// 이 클래스의 init이나 initWithFrame:메서드는 절대 호출되지 않는다.
// Nib파일들은 아카이브 객체들을 가지고 있기 때문에,Nib로부터 로딩되는 모든 객체는
// initWithCoder:메서드를 사용하여 초기활될 것이다.
-(id)initWithCoder:(NSCoder *)coder
{
	if(self = [super initWithCoder:coder])
	{
		//ball.png이미지를 로딩, 뷰의 중앙 좌료를 계산하여 그것을 구슬의 시작점으로 설정
		self.image = [UIImage imageNamed:@"ball.png"];
		self.currentPoint = CGPointMake((self.bounds.size.width / 2.0f) + (image.size.width / 2.0f), 
										(self.bounds.size.height / 2.0f) + (image.size.height /2.0f));
		
		// x,y축 방향에 대한 속도를 0으로 설정
		ballXVelocity = 0.0f;
		ballYVelocity = 0.0f;		
	}
	return self;	
}

#pragma mark -
-(CGPoint)currentPoint
{
	return currentPoint;
}

-(void)setCurrentPoint:(CGPoint)newPoint
{
	previousPoint = currentPoint;
	currentPoint = newPoint;
	
	// 화면의 경계선을 확인하는 작업
	// 만일 구슬의 x,y좌표 둘 중 어느 한쪽이라도 0보다 작어나 화면의 너비나 높이보다 크다면
	// (이미지의 너비와 높이를 계산에 포함시킨 상태에서), 가속도가 더 이상 작용하지 않는 상태로 처리하였다.
	if(currentPoint.x < 0)
	{
		currentPoint.x = 0;
		ballXVelocity = 0;
	}
	
	if(currentPoint.y < 0)
	{
		currentPoint.y = 0;
		ballYVelocity = 0;
	}
	
	if(currentPoint.x > self.bounds.size.width - image.size.width)
	{
		currentPoint.x = self.bounds.size.width - image.size.width;
		ballXVelocity = 0;
	}
	
	if(currentPoint.y > self.bounds.size.height - image.size.height)
	{
		currentPoint.y = self.bounds.size.height - image.size.height;
		ballYVelocity = 0;
	}
	
	// 2개의 CGRects 변수를 계산한다.
	// 하나의 직사각형은 새로운 이미지가 그려질 영역을 정의하고,
	// 다른 직사각형은 마지막으로 그려진 영역을 정의한다.
	// 2개의 직사각형 영역을 사용하여, 이전 영역에 그려진 구슬의 이미지를 지우고 새 영역에 구슬 이미지를 그릴것이다.
	CGRect currentImageRect = CGRectMake(currentPoint.x, currentPoint.y,
										 currentPoint.x + image.size.width,
										 currentPoint.y + image.size.height);
	
	CGRect previousImageRect = CGRectMake(previousPoint.x, previousPoint.y,
										  previousPoint.x + image.size.width,
										  previousPoint.y + image.size.height);
	
	// 2개의 직사각형을 결합한 새로운 직사각형을 만들었다.
	// 이 직사각형은 뷰에 다시 그려질 영역을 정의하기 위해 setNeedsDisplayInRect:의 인자로 사용된다.
	[self setNeedsDisplayInRect:CGRectUnion(currentImageRect, previousImageRect)];
		
}

// draw 메서드는 구슬의 새 위치를 정확히 계산하는데 사용된다.
// 이 메서드는 컨트롤러 클래스의 accelerometer메서드가 acceleration인자를 BallView 클래스에게
// 전달한 후에 accelerometer 메서드에게 호출된다.
-(void)draw
{
	// 먼저 NSDate 변수 하나를 static으로 선언한다.
	// 이변수는 마지막으로 draw메서드가 호출된 후부터 얼마나 지났는지를 체크하는데 사용된다.
	static NSDate *lastDrawTime;
	
	// draw메서드를 처음 호출할 때 lastDrawTime은 nil이며, lastDrawTime에 날짜가 
	// 할당되지 않아 메서드는 아무 일도 하지 않을 것이다. 
	// 업데이트는 1/60초의 주기로 발생하기 때문에, 처음 호출될 때 아무런 동작을 하지 않아도 알아채는 사람은 없을 것이다.
	if(lastDrawTime != nil)
	{
		// draw메서드가 호출될 때마다 매서드가 호출된 마지막 시간으로부터 현재 시간을 계산할 것이다.
		// lastDrawTime이 과거 시간이기 때문에 timeIntervalSineNow는  
		// lastDrawTime에서 현재 시간을 뺀 음수 값을 반활 할 것이다. 
		// 따라서 반환된 값에 마이너스(-)를 붙여서 secondsSineLastDraw에 저장하였다.		
		NSTimeInterval secondsSinceLastDraw = -([lastDrawTime timeIntervalSinceNow]);
		
		// 가속도를 현재 속도에 더하여 x,y방향의 속도를 계산하였다.
		// 가속도가 특정 시간 동안 지속되도록 가속도 값에 secondsSinceLastDraw를 곱하였다.
		ballYVelocity = ballYVelocity + -(acceleration.y * secondsSinceLastDraw);		
		ballXVelocity = ballXVelocity + acceleration.x * secondsSinceLastDraw;
	
		// draw메서드가 마지막으로 호출된 시점으로부터 지금까지의 속도 값을 사용하여 픽셀 단위의 
		// 위치 변화량을 계산하였다.
		// 구슬의 움직임을 자연스럽게 만들기 위해 속도 값과 경과 시간에 500을 곱하였다.
		// 만일 500을 곱하지 ㅇ낳는다면 구슬이 끈끈이에 달라붙은 것처럼 가속도가 엄청나게 느릴 것이다.
		CGFloat xAcceleration = secondsSinceLastDraw * ballXVelocity * 500;
		CGFloat yAcceleration = secondsSinceLastDraw * ballYVelocity * 500;
		
		self.currentPoint = CGPointMake(self.currentPoint.x + xAcceleration, self.currentPoint.y + yAcceleration);
	}
	
	// 현재 시간을 lastDrawTime를 업테이트 하였다.
	[lastDrawTime release];
	lastDrawTime = [[NSDate alloc] init];
		
}

// initWithCoder:메서드에서 로딩했던 구슬 이미지를 뷰에 그리는 기능만을 구현
- (void)drawRect:(CGRect)rect {
	[image drawAtPoint:currentPoint];
}

- (void)dealloc {
	[image release];
	[acceleration release];
    [super dealloc];
}

@end

: