Android Tutorial Introduction to the libGDX library

Introduction to the libGDX library

What is libGDX ?

libGDX is a game engine. As we saw in the first tutorial, a game engine provides a framework to create games and covers all aspects (rendering, animation, input, music, networking, physics, ...) of various kinds of games.

libGDX is considered as one of the best and fastest engine for the Android world. It is free, rich-featured, reliable, and proved its efficiency in a lot of well-known games (Ingress, Zombie Smasher, Apparatus, Monsterama Park, Clash of the Olympians, Bumbledore, etc.)

It’s a layered framework: it goes from low-level classes for OpenGL experts to high-level classes, easy to use by beginners. It includes a scene graph (Scene2D classes), a physics engine (Box2D classes), a particle system, a map renderer, a sprite batcher, an extensive set of mathematics classes… more than 200 classes in total.

For technical reasons, the version for Basic4Android cannot be multi-platform, and by choice, the current release doesn't include the 3D classes (except the ones for the perspective camera and for the decals) and the Daydream class.

libGDX was created in 2010 by Mario Zechner (badlogicgames.com) and is maintained by M. Zechner, Nathan Sweet (esotericsoftware.com) and a community of developers.

Minimum requirements

OpenGL ES 2.0
Android Froyo (API 8)

Hardware acceleration

libGDX does not require that you enable hardware acceleration on your device because it is based on OpenGL, which interacts directly with the GPU.

Debugging

You cannot use the debugger of B4A with libGDX because most of the code of the library runs in a different thread. You have to use the Log() function to debug your game.

The library provides a debug renderer for Box2D (lgBox2DDebugRenderer), a debug renderer for Scene2D (lgScn2DDebugRenderer) and a profiler for OpenGL (lgGLProfiler).

A word about the classes

The main class is LibGDX. All other classes are prefixed by lg.
All Box2D classes are prefixed by lgBox2D. All Scene2D classes are prefixed by lgScn2D. All Map classes are prefixed by lgMap. All Math classes are prefixed by lgMath.

The LibGDX class gives access to five interfaces: Audio (lgAudio), Files (lgFiles), Graphics (lgGraphics), Input (lgInput), and Net (lgNet). You will use the Input interface, for example, to get the input from the accelerometer.

With some classes (e.g. the five interfaces), you cannot create a new instance with Dim. You have to use an instance returned by the library. For example, you cannot write:
B4X:
Dim Graphics As lgGraphics
Graphics = lGdx.Graphics
but you can write:
B4X:
Dim Graphics As lgGraphics = lGdx.Graphics

Some classes cannot be instantiated at all because they are generic classes (e.g. com.badlogic.gdx.scenes.scene2d.utils.Drawable or com.badlogic.gdx.maps.tiled.TiledMapTile). In this case, either you store their instance as an Object or you use a subclass, e.g.:
B4X:
Dim objTile As Object = CurrentLayer.GetCell(X, Y).Tile
Dim staticTile As lgMapStaticTiledMapTile = CurrentLayer.GetCell(X, Y).Tile

OpenGL ES

I explained what OpenGL is in the previous tutorial and I won't discuss it further here because the main advantage to use a game engine like libGDX is to benefit from the abstraction layer above OpenGL. However, if you need (or want) to call directly OpenGL, here's how to get access to the classes and functions:
B4X:
Dim lGdx_GL20 As lgGL20 = lGdx.Graphics.GL20
or better (includes also the constants):
B4X:
Dim GL As lgGL

Note: libGDX uses the 2D coordinate system and the color encoding of OpenGL ES, so the Y-axis is pointing upwards and each color value ranges from 0 to 1.

The libGDX life-cycle

An important thing to keep in mind about libGDX is that it runs in its own thread. Your Basic4Android application runs most of the time in a different thread called the UI thread, or main thread. That means you cannot access the other views of your activity and change their properties from the libGDX thread. Fortunately, there's a function in the LibGDX class that passes the runnable (the piece of code to execute) from a thread to the other: CallSubUI. So if you want to change the activity title, set a label text or show a MsgBox from the libGDX thread, don't forget to use this function to avoid a crash!

Since libGDX runs in a different thread, you have to inform its library of the events of your activity : Create, Pause and Resume. First, create an instance of libGDX in Globals :
B4X:
Dim lGdx As libGDX
In Activity_Create (and nowhere else), add the Initialize function :
B4X:
lGdx.Initialize(False, "LG") 'fills the activity with the libGDX surface, uses OpenGL 1 for compatibility and prefixes the events with LG
In Activity_Resume, add the following line :
B4X:
If lGdx.IsInitialized Then lGdx.Resume
In Activity_Pause, add the following line :
B4X:
If lGdx.IsInitialized Then lGdx.Pause

You could initialize libGDX differently. For example, it could be limited to a view with InitializeView. You could also define a configuration (lgConfiguration class) and pass it to libGDX. Example:
B4X:
Dim Config As lgConfiguration

'Disables the accelerometer and the compass
Config.useAccelerometer = False
Config.useCompass = False

'Uses a WakeLock (the device will stay on)
Config.useWakelock = True

'Creates the libGDX surface
lGdx.Initialize2(Config, "LG")

Once done, your library is ready to raise the events of its life-cycle : Create, Resize, Render, Pause, Resume, Dispose. These events are the place to put all the code of your game. Don't put anything in the usual activity events. They are reserved for your other views and are raised by the UI thread.

Create :

Create is the first raised event. It is raised soon after the initialization of the library and the creation of the OpenGL surface.
In this event, initialize your renderer and your input processors, load your resources (we'll see that in detail later) and initialize your game data.

Resize :

Resize is raised when the size of the libGDX surface changes. Under Android, that should only happen when you start the application and when it is restarted after a rotation or resumed.
It is raised at least once, after Create, and, when the application is resumed, just before Resume.
In this event, initialize the camera viewport. It's probably the only use you will find for it.
This event returns the new width and height in pixels.

Render :

Render is raised as soon as possible after Create and Resize.
It's where things are drawn. It's also where you have to put the logic of your game, but I would not recommend putting hundreds of lines of code here. Instead, create new subs and new modules and call them from this event.
The first lines in Render should be to clear the screen. Example:
B4X:
lGdx_GL.glClearColor(0, 0, 1, 1) 'Blue background
lGdx_GL.glClear(lGdx_GL.GL10_COLOR_BUFFER_BIT)

Pause :

Pause is raised when the activity is sent in the background, rotated or exited.
It's the right place to save your game data.
Note that the OpenGL context is destroyed when the app goes in the background, so all your unmanaged textures and pixmaps have to be reloaded or recreated in the Resume event when the app returns to the foreground.

Resume :

Contrary to the Resume event of your activity, this one is not raised after Create, only when the application returns from a pause.
As the OpenGL context is destroyed when the app goes in the background, all your unmanaged* textures and pixmaps have to be reloaded or recreated when this event is raised. More info here. *Not loaded by an asset manager.

Dispose :

Dispose is called when the activity is exited, after Pause, or when the device is rotated.
In this event, release all the used resources by calling the Dispose function of objects (if they have one).

The life-cycle :
application_lifecycle_diagram.png


Multiple screens

A game is made of many screens. You could create an activity for each one, but that would not be very convenient because you'd have to reinitialize the library in each activity and reload some resources. Moreover, that would not ease any graphical transition between screens. In fact, most games are made with a very small number of activities and make use of a screen manager instead. The screen manager stores the reference of the different screens and allows switching between them. Each screen has its own life-cycle.
To create a screen manager with two screens, for example, declare them in Globals:
B4X:
Dim lGdx_ScrMgr As lgScreenManager
Dim lGdx_Screen(2) As lgScreen
Then add these lines in the Create event handler:
B4X:
'Creates two screens
lGdx_ScrMgr.Initialize(lGdx)
lGdx_Screen(0) = lGdx_ScrMgr.AddScreen("LGS1")
lGdx_Screen(1) = lGdx_ScrMgr.AddScreen("LGS2")
Show the first screen:
B4X:
lGdx_ScrMgr.CurrentScreen = lGdx_Screen(0)

When you want to change the current screen, just change the value of the CurrentScreen property. That will raise the Hide event of the previous screen and the Show event of the new one.

The screens have the same life-cycle as the library, and thus the same events except that Create is named Show and Dispose is named Hide.

Input processor and gesture detector

To get the input events raised by your players, you have to declare input processors. libGDX has an input processor for keyboard and touch events (lgInputProcessor) and a specialized input processor for gestures (lgGestureDetector).
Start by declaring them in Globals:
B4X:
Dim lGdx_IP As lgInputProcessor
Dim lGdx_GD As lgGestureDetector
Initialize them in the Create event (or the Show event of a screen if you want different processors for different screens):
B4X:
lGdx_IP.Initialize("IP")
lGdx_GD.Initialize("GD")
And add the event handlers that you need:
B4X:
Sub IP_KeyDown(KeyCode As Int) As Boolean
   Return False
End Sub

Sub IP_KeyUp(KeyCode As Int) As Boolean
   Return False
End Sub

Sub IP_KeyTyped(Character As Char) As Boolean
   Return False
End Sub

Sub IP_TouchDown(ScreenX As Int, ScreenY As Int, Pointer As Int) As Boolean
   Return False
End Sub

Sub IP_TouchDragged(ScreenX As Int, ScreenY As Int, Pointer As Int) As Boolean
   Return False
End Sub

Sub IP_TouchUp(ScreenX As Int, ScreenY As Int, Pointer As Int) As Boolean
   Return False
End Sub

Sub GD_TouchDown(X As Float, Y As Float, Pointer As Int) As Boolean
   Return False
End Sub

Sub GD_Fling(VelocityX As Float, VelocityY As Float) As Boolean
   Return False
End Sub

Sub GD_LongPress(X As Float, Y As Float) As Boolean
   Return False
End Sub

Sub GD_Pan(X As Float, Y As Float, DeltaX As Float, DeltaY As Float) As Boolean
   Return False
End Sub

Sub GD_Pinch(InitialPointer1 As lgMathVector2, InitialPointer2 As lgMathVector2, Pointer1 As lgMathVector2, Pointer2 As lgMathVector2) As Boolean
   Return False
End Sub

Sub GD_Tap(X As Float, Y As Float, Count As Int) As Boolean
   Return False
End Sub

Sub GD_Zoom(InitialDistance As Float, Distance As Float) As Boolean
    Return False
End Sub

Description of events :

Fling: The user quickly dragged a finger across the screen, then lifted it. Useful to implement swipe gestures.
Pan: The user is dragging a finger across the screen. The detector reports the current touch coordinates as well as the delta between the current and previous touch positions. Useful to implement camera panning in 2D.
Pinch: Similar to zoom. The detector reports the initial and current finger positions instead of the distance. Useful to implement camera zooming and more sophisticated gestures such as rotation.
Tap: The user touched the screen and lifted the finger. The finger must not move outside a specified square area around the initial touch position for a tap to be registered. Multiple consecutive taps will be detected if the user performs taps within a specified time interval.
Zoom: The user has placed two fingers on the screen and is moving them together/apart. The detector reports both the initial and current distance between fingers in pixels. Useful to implement camera zooming.

The other events are self-explanatory.

.....
 
Last edited:

rkwan

Member
Licensed User
Thanks, Informatix!

I can finally see it after I put the point along one of the axes and use a bigger tablet to show!

Therefore, my next questions would be:-

1. Any easy way to make the dot bigger to view please?!

2. In fact, I am trying to do a 3D scatter plot like this one with matplotlib:-
https://matplotlib.org/examples/mplot3d/scatter3d_demo.html
I can get the elevation and azimuth view change with sliders.
Therefore, I am thinking if I can achieve the same with B4A
but it seems quite difficult now...?!

Any suggestion please?

Or do you think that the 3D Bodies library may be better for this kind of plot please?
https://www.b4x.com/android/forum/threads/3d-bodies-four-libraries.48373/

Thanks & Best Regards,
Robert
 

Informatix

Expert
Licensed User
Longtime User
Thanks, Informatix!

I can finally see it after I put the point along one of the axes and use a bigger tablet to show!

Therefore, my next questions would be:-

1. Any easy way to make the dot bigger to view please?!

2. In fact, I am trying to do a 3D scatter plot like this one with matplotlib:-
https://matplotlib.org/examples/mplot3d/scatter3d_demo.html
I can get the elevation and azimuth view change with sliders.
Therefore, I am thinking if I can achieve the same with B4A
but it seems quite difficult now...?!

Any suggestion please?

Or do you think that the 3D Bodies library may be better for this kind of plot please?
https://www.b4x.com/android/forum/threads/3d-bodies-four-libraries.48373/

Thanks & Best Regards,
Robert
Draw boxes instead of points.
 

rkwan

Member
Licensed User
Thanks, Informatix!

I can finally create my Scatter Plot like attached now.

However, I have another challenge which I would like to seek for your advice please.

In my mat3dplot, I could use sliders to control the azimuth and elevation
but now I can only control the azimuth through the Y-axis rotation in this example code.

Therefore, I would like to ask if any way to distinguish horizontal touch dragging vs vertical touch dragging in Sub IP_TouchDragged
so that I can get rotation along X-axis for vertical touch dragging to mimick the elevation view in mat3dplot please.


Thanks & Best Regards,
 

Attachments

  • Screenshot_20180328-182023.png
    Screenshot_20180328-182023.png
    23.6 KB · Views: 496

Informatix

Expert
Licensed User
Longtime User
Thanks, Informatix!

I can finally create my Scatter Plot like attached now.

However, I have another challenge which I would like to seek for your advice please.

In my mat3dplot, I could use sliders to control the azimuth and elevation
but now I can only control the azimuth through the Y-axis rotation in this example code.

Therefore, I would like to ask if any way to distinguish horizontal touch dragging vs vertical touch dragging in Sub IP_TouchDragged
so that I can get rotation along X-axis for vertical touch dragging to mimick the elevation view in mat3dplot please.


Thanks & Best Regards,
In the ShapeRender demo, LastX - ScreenX computes the amount of move on the horizontal axis. Do the same for the vertical axis (LastY - ScreenY in IP_TouchDragged, with LastY initialized in IP_Touchdown). By comparing the absolute value of the two deltas, you can decide whether the move is more horizontal than vertical, or the contrary.
 

rkwan

Member
Licensed User
Thanks a lot for the hint, Informatix!

I can manage to have the X-axis rotation now but it would not be isolated from the Y-axis rotation even when I slide vertically only.
It would still mix up with Y-axis rotation at the same time (even though it is supposed to be triggered by horizontal slide?!)
and so my display would become messy rotation.

If I comment out either the if or the else if section before,
I can get the Y-axis and X-axis rotation independently Ok with either horizontal or vertical sliding though.

B4X:
Sub Globals

    Dim LastX As Float
    Dim Point, Y_Axis As lgMathVector3
    Dim RotMatrix As lgMathMatrix4
    Dim LastY As Float
    Dim X_Axis As lgMathVector3
    Dim RotMatrix2 As lgMathMatrix4
   
    Dim Y_Rot_Only As Int
    Dim X_Rot_Only As Int
End Sub

Sub LG_Create
   ...
    'Initializes the vector of the rotation axis
    Y_Axis.Set(0, 1, 0)

    X_Axis.Set(1, 0, 0)

End Sub

Sub IP_TouchDown(ScreenX As Int, ScreenY As Int, Pointer As Int) As Boolean
    If Pointer <> 0 Then Return False

    LastX = ScreenX
    LastY = ScreenY
   
    Y_Rot_Only = 0
    X_Rot_Only = 0
   
    Return True
End Sub


Sub IP_TouchDragged(ScreenX As Int, ScreenY As Int, Pointer As Int) As Boolean
    If Pointer <> 0 Then Return False

    'Rotates the camera on the Y axis
    Point.Set(Camera.Position.X, Camera.Position.Y, Camera.Position.Z)
   
    If (Abs(LastX - ScreenX) > Abs(LastY - ScreenY)) And (X_Rot_Only <> 1) Then
        Log("Path YYY")
        RotMatrix.setToRotation(Y_Axis, (LastX - ScreenX) / 5)
        Point.Mul(RotMatrix)
        Camera.Position.Set(Point.X, Point.Y, Point.Z)
        Camera.LookAt(0.0, 0.5, 0.0)
        Camera.Update
        Y_Rot_Only = 1
    Else If (Abs(LastX - ScreenX) <= Abs(LastY - ScreenY)) And (Y_Rot_Only <> 1) Then
        Log("Path XXX")
        RotMatrix2.setToRotation(X_Axis, (LastY - ScreenY) / 5)
        Point.Mul(RotMatrix2)
        Camera.Position.Set(Point.X, Point.Y, Point.Z)
        Camera.LookAt(0.5, 0.5, 0.5)
        Camera.Update
        X_Rot_Only = 1
    End If
   
    'LastX = ScreenX
    'LastY = ScreenY
    Return True
End Sub

[\code]

Therefore, any idea on what I am wrong please?


Thanks & Best Regards,
 

Informatix

Expert
Licensed User
Longtime User
Thanks a lot for the hint, Informatix!

I can manage to have the X-axis rotation now but it would not be isolated from the Y-axis rotation even when I slide vertically only.
It would still mix up with Y-axis rotation at the same time (even though it is supposed to be triggered by horizontal slide?!)
and so my display would become messy rotation.

If I comment out either the if or the else if section before,
I can get the Y-axis and X-axis rotation independently Ok with either horizontal or vertical sliding though.

B4X:
Sub Globals

    Dim LastX As Float
    Dim Point, Y_Axis As lgMathVector3
    Dim RotMatrix As lgMathMatrix4
    Dim LastY As Float
    Dim X_Axis As lgMathVector3
    Dim RotMatrix2 As lgMathMatrix4
  
    Dim Y_Rot_Only As Int
    Dim X_Rot_Only As Int
End Sub

Sub LG_Create
   ...
    'Initializes the vector of the rotation axis
    Y_Axis.Set(0, 1, 0)

    X_Axis.Set(1, 0, 0)

End Sub

Sub IP_TouchDown(ScreenX As Int, ScreenY As Int, Pointer As Int) As Boolean
    If Pointer <> 0 Then Return False

    LastX = ScreenX
    LastY = ScreenY
  
    Y_Rot_Only = 0
    X_Rot_Only = 0
  
    Return True
End Sub


Sub IP_TouchDragged(ScreenX As Int, ScreenY As Int, Pointer As Int) As Boolean
    If Pointer <> 0 Then Return False

    'Rotates the camera on the Y axis
    Point.Set(Camera.Position.X, Camera.Position.Y, Camera.Position.Z)
  
    If (Abs(LastX - ScreenX) > Abs(LastY - ScreenY)) And (X_Rot_Only <> 1) Then
        Log("Path YYY")
        RotMatrix.setToRotation(Y_Axis, (LastX - ScreenX) / 5)
        Point.Mul(RotMatrix)
        Camera.Position.Set(Point.X, Point.Y, Point.Z)
        Camera.LookAt(0.0, 0.5, 0.0)
        Camera.Update
        Y_Rot_Only = 1
    Else If (Abs(LastX - ScreenX) <= Abs(LastY - ScreenY)) And (Y_Rot_Only <> 1) Then
        Log("Path XXX")
        RotMatrix2.setToRotation(X_Axis, (LastY - ScreenY) / 5)
        Point.Mul(RotMatrix2)
        Camera.Position.Set(Point.X, Point.Y, Point.Z)
        Camera.LookAt(0.5, 0.5, 0.5)
        Camera.Update
        X_Rot_Only = 1
    End If
  
    'LastX = ScreenX
    'LastY = ScreenY
    Return True
End Sub

[\code]

Therefore, any idea on what I am wrong please?


Thanks & Best Regards,
When more than one axis is involved, that becomes more complicated. Look at this solution:
https://stackoverflow.com/questions/30690027/realistic-rotation-of-a-3d-model-in-libgdx-by-dragging
 

rkwan

Member
Licensed User
Thanks for the information, Informatix!

Yes, it looks complicated and I will need some more time to try out then...
 

rkwan

Member
Licensed User
Hello Informatix,

After spending some more time to try out the solution you pointed,
I am still stuck at two points and would like to see if you would have more pointers please.

First, I mimick that solution to implement in B4A like this:-

B4X:
    Dim Ray1, Ray2 As lgMathRay
    Dim Screen1, Screen2 As lgMathVector3
    Dim Cross1, Cross2 As lgMathVector3
    Dim CenterPosition As lgMathVector3
    Dim QRotation As lgMathMatrix4

Sub IP_TouchDragged(ScreenX As Int, ScreenY As Int, Pointer As Int) As Boolean
    If Pointer <> 0 Then Return False

    'Rotates the camera on the Y axis
    Point.Set(Camera.Position.X, Camera.Position.Y, Camera.Position.Z)

    Ray1 = Camera.GetPickRay(LastX, LastY)
    Ray2 = Camera.GetPickRay(ScreenX, ScreenY)
   
    Screen1 = Ray1.direction.cpy().scl(0.1).add(Ray1.origin)
    Screen2 = Ray2.direction.cpy().scl(0.1).add(Ray2.origin)

    CenterPosition.set(0,0,0)   

    Cross1 = Screen1.cpy().sub(CenterPosition).nor()
    Cross2 = Screen2.cpy().sub(CenterPosition).nor()
   
    '    RotMatrix.setToRotation(Cross1, Cross2)
    QRotation.set

    Point.Mul(RotMatrix)
    Camera.Position.Set(Point.X, Point.Y, Point.Z)
    Camera.LookAt(0.0, 0.5, 0.0)
    Camera.Update

...

[\code]

1. 
for the settings of Ray1, Ray2, Screen1 & Screen2 seem straight-forward,
but I am not sure if the myObjectsCenterPosition in the solution would mean,
 what I am trying to set CenterPosition.set(0,0,0) as in your example with the 3 axes?!

2. I know that there is no Quaternion class in the libGDX and so I tried to use lgMathMatrix4 instead
but then I realized that there is no member setfFromCross(Cross1, Cross2) for the lgMathMatrix4 as in Quaternion,
and so I am stuck here too.


Thanks & Best Regards,
 

Informatix

Expert
Licensed User
Longtime User
Hello Informatix,

After spending some more time to try out the solution you pointed,
I am still stuck at two points and would like to see if you would have more pointers please.

First, I mimick that solution to implement in B4A like this:-

B4X:
    Dim Ray1, Ray2 As lgMathRay
    Dim Screen1, Screen2 As lgMathVector3
    Dim Cross1, Cross2 As lgMathVector3
    Dim CenterPosition As lgMathVector3
    Dim QRotation As lgMathMatrix4

Sub IP_TouchDragged(ScreenX As Int, ScreenY As Int, Pointer As Int) As Boolean
    If Pointer <> 0 Then Return False

    'Rotates the camera on the Y axis
    Point.Set(Camera.Position.X, Camera.Position.Y, Camera.Position.Z)

    Ray1 = Camera.GetPickRay(LastX, LastY)
    Ray2 = Camera.GetPickRay(ScreenX, ScreenY)
  
    Screen1 = Ray1.direction.cpy().scl(0.1).add(Ray1.origin)
    Screen2 = Ray2.direction.cpy().scl(0.1).add(Ray2.origin)

    CenterPosition.set(0,0,0)  

    Cross1 = Screen1.cpy().sub(CenterPosition).nor()
    Cross2 = Screen2.cpy().sub(CenterPosition).nor()
  
    '    RotMatrix.setToRotation(Cross1, Cross2)
    QRotation.set

    Point.Mul(RotMatrix)
    Camera.Position.Set(Point.X, Point.Y, Point.Z)
    Camera.LookAt(0.0, 0.5, 0.0)
    Camera.Update

...

[\code]

1.
for the settings of Ray1, Ray2, Screen1 & Screen2 seem straight-forward,
but I am not sure if the myObjectsCenterPosition in the solution would mean,
 what I am trying to set CenterPosition.set(0,0,0) as in your example with the 3 axes?!

2. I know that there is no Quaternion class in the libGDX and so I tried to use lgMathMatrix4 instead
but then I realized that there is no member setfFromCross(Cross1, Cross2) for the lgMathMatrix4 as in Quaternion,
and so I am stuck here too.


Thanks & Best Regards,
This is a bit out of my league. I'm not an expert in 3D maths.
Sorry for the lack of the Quaternion class. I completely forgot that it was not exposed publicly in the B4A version.
 

rkwan

Member
Licensed User
Thanks, Informatix.

I will just have to wait for the Quaternion class later then
as I am also not expert in the 3D maths!


Thanks & Best Regards,
 

Qolam

New Member
hello. ive got a question if u could answer. i want to do smt like this:
type pic(left as float , .... , texture as lgtexture)
dim pics() as pic


and then use it as :
pics(0).initialize
pics(0).texture.initialize("pic.png")
but it bugs out. already tried this too
dim temptexture as lgtexture
temptexture.initialize("pic.png")
pics(0).texture=temptexture
but this aint working.
my point was to make a sub and make creating buttons and stuff inside libgdx easier.
 
Top