Android Tutorial OSMDroid - MapView for B4A tutorial

You can find the OSMDroid library thread here: http://www.b4x.com/forum/additional...tes/16309-osmdroid-mapview-b4a.html#post92643.

AIM: Create and initialize a MapView, enable the map zoom controller and multitouch controller, set a zoom level then center the map on a location.

B4X:
Sub Process_Globals
End Sub

Sub Globals
   Dim MapView1 As MapView
End Sub

Sub Activity_Create(FirstTime As Boolean)
   If File.ExternalWritable=False Then
      '   OSMDroid requires the use of external storage to cache tiles
      '   if no external storage is available then the MapView will display no tiles
      Log("WARNING NO EXTERNAL STORAGE AVAILABLE")
   End If
   
   '   no EventName is required as we don't need to listen for MapView events
   MapView1.Initialize("")
   Activity.AddView(MapView1, 0, 0, 100%x, 100%y)
   
   '   by default the map will zoom in on a double tap and also be draggable - no other user interface features are enabled
   
   '   enable the built in zoom controller - the map can now be zoomed in and out
   MapView1.SetZoomEnabled(True)
   
   '   enable the built in multi touch controller - the map can now be 'pinch zoomed'
   MapView1.SetMultiTouchEnabled(True)
   
   '   set the zoom level BEFORE the center (otherwise unpredictable map center may be set)
   MapView1.Zoom=14
   MapView1.SetCenter(52.75192, 0.40505)
End Sub

Sub Activity_Resume
End Sub

Sub Activity_Pause (UserClosed As Boolean)
End Sub

The code is pretty self-explanatory.

I've added the code to check if the device has available external storage as OSMDroid will not display any tiles if no external storage is available.
External storage is used to save/cache all downloaded tiles - no external storage means no map!
(I'll omit that check from future tutorials but it's something to bear in mind - not that i know of any Android devices that have no external storage).

Create and initialize a MapView, add it to the Activity 100% width and 100% height.
Enable the zoom and multi-touch controller.
Zoom in to level 14 then set the MapView center to a location (sunny Norfolk, UK!).

I've found that setting the map center and then immediately setting the zoom level does not work as expected.
I think that while the MapView is setting the map center it also zooms in so the end result is unpredictable.

Pan and zoom the map, now rotate your device and you'll see the map returns to it's initial state of zoom level 14, center (52.75192, 0.40505).

I shall show you how to save and restore the MapView state next...

Martin.
 

Attachments

  • 01 - SimpleMap.zip
    5.8 KB · Views: 4,649
Last edited:

Jonas

Member
Licensed User
Longtime User
I've attached an update that i hope will fix your problem.
Can you test it and let me know if it works?

Creating a List with GeoPoints and add it to the PathOverlay is working now.
But I thought that using PathOverlay.AddPoints(GeoPointList) will create an independent path and next time I use .AddPoints a new independent path will be created. But that was not the case, it will continue to draw from the last point in the previous list. Ended up with Spaghetti all over the map. So a new PathOverlay needs to be created for each independent path I'm going to draw. But as you say, I can put the PathOverlays in a map and then run through them when hiding and showing the paths. But I haven't figured out how I will approach this yet.

Thanks for the update!
/J
 

warwound

Expert
Licensed User
Longtime User
Creating a List with GeoPoints and add it to the PathOverlay is working now.
But I thought that using PathOverlay.AddPoints(GeoPointList) will create an independent path and next time I use .AddPoints a new independent path will be created. But that was not the case, it will continue to draw from the last point in the previous list. Ended up with Spaghetti all over the map. So a new PathOverlay needs to be created for each independent path I'm going to draw. But as you say, I can put the PathOverlays in a map and then run through them when hiding and showing the paths. But I haven't figured out how I will approach this yet.

Thanks for the update!
/J

The PathOverlay has a ClearPath method - you can use this to clear existing points and then re-use the PathOverlay adding new path points.

Martin.
 

Jonas

Member
Licensed User
Longtime User
Just three questions, would it be possible to save a GeoPoint to file? Like inside a Map or List? Or is the GeoPoint a complex Type?
 

warwound

Expert
Licensed User
Longtime User
I just did a simple test and yes you can save a GeoPoint to a file.

Take a look at this code, it requires the RandomAccessFile library to be enabled:

B4X:
Sub Process_Globals
   Dim ZoomLevel As Int
End Sub

Sub Globals
   Dim MapView1 As MapView
End Sub

Sub Activity_Create(FirstTime As Boolean)
   MapView1.Initialize("")
   '   MapView1.SetDataConnectionEnabled(False)
   MapView1.SetZoomEnabled(True)
   MapView1.SetMultiTouchEnabled(True)
   Activity.AddView(MapView1, 0, 0, 100%x, 100%y)
   
   Dim MapCenter As GeoPoint
   If File.Exists(File.DirInternal, "saved_geopoint.dat") Then
      Dim RandomAccessFile1 As RandomAccessFile
      RandomAccessFile1.Initialize(File.DirInternal, "saved_geopoint.dat", True)
      MapCenter=RandomAccessFile1.ReadObject(0)
      RandomAccessFile1.Close
   Else
      '   set the default initial map center and zoom level
      MapCenter.Initialize(35.241, 24.79)
   End If
   
   If FirstTime Then
      ZoomLevel=9
   End If
   
   MapView1.Zoom=ZoomLevel
   MapView1.SetCenter3(MapCenter)
End Sub

Sub Activity_Resume
End Sub

Sub Activity_Pause (UserClosed As Boolean)
   '   save the current map zoom level and center
   Dim RandomAccessFile1 As RandomAccessFile
   RandomAccessFile1.Initialize(File.DirInternal, "saved_geopoint.dat", False)
   RandomAccessFile1.WriteObject(MapView1.GetCenter, True, 0)
   RandomAccessFile1.Close
   ZoomLevel=MapView1.Zoom
End Sub

This is one of the tutorial examples where originally the map center was saved to a process global GeoPoint.
I've replaced that, saving the GeoPoint to a file and restoring it from file if the file exists and it works.

The native java GeoPoint class implements Serializable which is why (i think) you can save it to a file.

The above code can also be updated to save the map zoom level to the same file:

B4X:
Sub Process_Globals

End Sub

Sub Globals
   Dim MapView1 As MapView
End Sub

Sub Activity_Create(FirstTime As Boolean)
   MapView1.Initialize("")
   '   MapView1.SetDataConnectionEnabled(False)
   MapView1.SetZoomEnabled(True)
   MapView1.SetMultiTouchEnabled(True)
   Activity.AddView(MapView1, 0, 0, 100%x, 100%y)
   
   Dim MapCenter As GeoPoint
   Dim ZoomLevel As Int
   
   If File.Exists(File.DirInternal, "saved_geopoint.dat") Then
      Dim RandomAccessFile1 As RandomAccessFile
      RandomAccessFile1.Initialize(File.DirInternal, "saved_geopoint.dat", True)
      MapCenter=RandomAccessFile1.ReadObject(0)
      ZoomLevel=RandomAccessFile1.ReadInt(RandomAccessFile1.CurrentPosition)
      RandomAccessFile1.Close
   Else
      '   set the default initial map center and zoom level
      MapCenter.Initialize(35.241, 24.79)
      ZoomLevel=9
   End If
   
   MapView1.zoom=ZoomLevel
   MapView1.SetCenter3(MapCenter)
End Sub

Sub Activity_Resume
End Sub

Sub Activity_Pause (UserClosed As Boolean)
   '   save the current map zoom level and center
   Dim RandomAccessFile1 As RandomAccessFile
   RandomAccessFile1.Initialize(File.DirInternal, "saved_geopoint.dat", False)
   RandomAccessFile1.WriteObject(MapView1.GetCenter, True, 0)
   RandomAccessFile1.WriteInt(MapView1.Zoom, RandomAccessFile1.CurrentPosition)
   RandomAccessFile1.Close
End Sub

The map now restores it's last center and zoom level not just on orientation change but also when the app is restarted.

Martin.
 

Attachments

  • SaveGeoPoint.zip
    5.8 KB · Views: 430

warwound

Expert
Licensed User
Longtime User
Jonas.

I was just wondering if you plan to save a List of GeoPoints for those paths you mentioned?

It might be possible to create a file for each list of GeoPoints (ie each Path) on a device or emulator and then include that already saved file with your compiled app.
Save the app having to initially create each List of GeoPoints and save to file.

Martin.
 

cirollo

Active Member
Licensed User
Longtime User
a little help on 2 questions....

Hi Martin,

I'm using with great satisfaction your library, thanks for sharing it!

but i'm stuck with 2 little problems:

the first...
I add markers through a web service in this way:

B4X:
Sub hc2_ResponseSuccess (Response As HttpResponse, TaskId As Int)
   Dim res As String
   res = Response.GetString("UTF8")
   Log("Response from server: " & res)
   Dim parser As JSONParser
   parser.Initialize(res)
   Dim Icon As BitmapDrawable   
   MarkersOverlay1.RemoveMarkers
'   MarkersFocusOverlay1.RemoveMarkers
   'add the markers to the ListView
   Dim nearfriends As List
   nearfriends.Initialize
   nearfriends = parser.NextArray 'returns a list with maps
   Dim Markers As List
   Markers.Initialize
   For i = 0 To nearfriends.Size - 1
      Dim m As Map
      m = nearfriends.Get(i)
      'markers
      Dim Marker1 As Marker
      '   Msgbox(m.Get("imei")&" - "&Main.Imeicode,"")
      If m.Get("imei")<> Main.Imeicode Then
         Icon.Initialize(LoadBitmap(File.DirAssets, "mapicon.png"))
      Else
         Icon.Initialize(LoadBitmap(File.DirAssets, "person.png"))
      '    MapView1.Zoom=14
          MapView1.SetCenter(Main.Lat, Main.Lon)
      End If
      Marker1.Initialize(m.Get("name"),m.Get("note"), m.Get("lat"),m.Get("lng"), Icon)
      Markers.Add(Marker1)
   Next      
   ' add the List of Markers to the MarkersOverlay
      MarkersOverlay1.AddMarkers(Markers)
'   MarkersFocusOverlay1.AddMarkers(Markers)
   ProgressDialogHide
'   MapView1.ZoomIn
'   MapView1.ZoomOut
   Response.Release
End Sub

but the markers don't appear until I do some actions, such as zoom in, then they appear properly.....



the second one.....
this is how i defined the mapiew object

B4X:
Sub Activity_Create(FirstTime As Boolean)
   Activity.LoadLayout("Mappa")
   BtnIndietro.Visible=True
   BtnNascondi.Visible=True
   BtnAggiorna.Visible=True
   LblPosizione.Text="La tua posizione: Lat "&Main.Lat&" Lon "&Main.Lon
   If FirstTime Then
      GPS1.Initialize("GPS")
      hc1.Initialize("hc1")
      hc2.Initialize("hc2")
   End If   
   GPS1.Start(0, 0)
 
   If File.ExternalWritable=False Then
        '    OSMDroid requires the use of external storage to cache tiles
        '    if no external storage is available then the MapView will display no tiles
        Log("WARNING NO EXTERNAL STORAGE AVAILABLE")
    End If
    
    MapView1.Initialize("")
    Activity.AddView(MapView1, 0, 50dip, 100%x, 100%y-70dip)
   LblPosizione.SetLayout( 0, 100%y-20dip, 100%x, 20dip)
    MapView1.SetZoomEnabled(True)
    MapView1.SetMultiTouchEnabled(True)
    MapView1.Zoom=14
    MapView1.SetCenter(Main.Lat, Main.Lon)
   MapView1.SetTileSource("Mapnik")
    ScaleBarOverlay1.Initialize(MapView1)
    MapView1.AddOverlay(ScaleBarOverlay1)
   MarkersOverlay1.Initialize(MapView1, "MarkersOverlay1")
    MapView1.AddOverlay(MarkersOverlay1)
'   MarkersFocusOverlay1.Initialize(MapView1)
'   MapView1.AddOverlay(MarkersFocusOverlay1)

   FetchFriendsList
   Timer1.Initialize("Timer1", 600000) ' 1000 = 1 secondo, 600000=600 secondi pari a 10 minuti
    Timer1.Enabled = True
   ImgHide.Visible=False
End Sub

so the map doesn't take all the layout, is smaller.
the problem is that when I zoom in or out of the map, it goes full screen, then returns to the position defined.
So, is possible to fix the dimensions during zoom?

hope to be clear in the explanation....

regards
ciro
 

warwound

Expert
Licensed User
Longtime User
Hi there cirollo.

The solution to the first question is to call the MapView Invalidate method AFTER adding your Markers:

B4X:
MarkersOverlay1.AddMarkers(Markers)

' here you need to call the MapView Invalidate method

The code you posted contains no reference to the instance of the MapView so i'll leave you to work that out - maybe you have a global reference to the MapView instance?

Your second question can be solved by placing your MapView in a Panel.
When the MapView is zoomed it animates to it's containing parent container.
Place the MapView in a Panel and when the MapView zooms it will animate within that Panel (instead of the entire Activity):

B4X:
'    Activity.AddView(MapView1, 0, 50dip, 100%x, 100%y-70dip)
Dim MapViewPanel As Panel
MapViewPanel.Initialize("")
Activity.AddView(MapViewPanel, 0, 50dip, 100%x, 100%y-70dip)
MapViewPanel.AddView(MapView1, 0, 0, 100%x, 100%y

Martin.
 

cirollo

Active Member
Licensed User
Longtime User
thank you!!

everything works fine!!!

the only strange thing it that, after putting the map in a panel, the zoom controls no longer appears

but is no important, the multitouch is still working!
 

warwound

Expert
Licensed User
Longtime User
Maybe the 100%y Height corresponds to 100% Activity Height not 100% Panel Height?
Might be better to set the MapView Width and Height to the Panel Width and Height:

B4X:
Activity.AddView(MapViewPanel, 0, 50dip, 100%x, 100%y-70dip)
MapViewPanel.AddView(MapView1, 0, 0, MapViewPanel.Width, MapViewPanel.Height)

Martin.
 

BW17

Member
Licensed User
Longtime User
how hide balloons

Hi there,

Sorry for my very bad english :signOops:

In my project, i use a mapview to display markers around the center of the map, and i have a button to refresh markers when i move the map center.

When i click a marker to open the balloon it works fine.

If i click another marker, the balloon closed and the new balloon is open, that's OK.

But il if move the map without closing the last open balloon, and refresh markers by my button, the balloon stay open, and it's impossible to close it.

I can open and close others balloons, but the old balloon stay open.

Has anyone an idee for solving it ?

Thanks for advance.

regards.
BW17
 

warwound

Expert
Licensed User
Longtime User
In theory that can't happen - the MarkersBalloonOverlay has just one balloon and there's no way to open more than one balloon.

So i'd guess that when you click your button and refresh your Markers you are removing all Markers from an existing instance of the MarkersBalloonOverlay then creating a new instance of a MarkersBalloonOverlay.
BUT you are not closing the open balloon that belongs to that existing instance of the MarkersBalloonOverlay.

Again that's shouldn't happen - both RemoveMarker and RemoveMarkers methods should clear the balloon if it is visible.

I'd guess that you have an instance of MarkersBalloonOverlay and remove it from the MapView using RemoveOverlay or RemoveOverlays before the MapView has redrawn itself - leaving the balloon visible.
Can you look in your code to see if you call MarkersBalloonOverlay RemoveOverlay or RemoveOverlays?
If so then before calling that method call the MapView Invalidate method:

B4X:
MyMapView.Invalidate

That should cause the MapView to redraw itself without the balloon and then the rest of your code can proceed as it does.

Martin.
 

BW17

Member
Licensed User
Longtime User
Many thanks Warwound,

It works fine now :sign0098:

Just another question : custom tiles zipfiles (for offline work) are in /osmdroid folder. Is it possible to place them in another folder ?

If yes, how to declare the new path and where ?

Thanks again.

BW17.
 

warwound

Expert
Licensed User
Longtime User
I'm afraid the cache folder is hardcoded to external storage 'osmdroid' and cannot be changed.

The logic being that all apps that use OSMDroid on the same device benefit from the shared fixed location cache.

Martin.
 

drgottjr

Expert
Licensed User
Longtime User
tests for external storage

The code is pretty self-explanatory.

I've added the code to check if the device has available external storage as OSMDroid will not display any tiles if no external storage is available.
External storage is used to save/cache all downloaded tiles - no external storage means no map!
(I'll omit that check from future tutorials but it's something to bear in mind - not that i know of any Android devices that have no external storage).
Martin.


my nexus 4 phone sold by google has no sd slot. it comes with 16gb of writeable memory. ditto the 8gb model. thanks for any help.
 

warwound

Expert
Licensed User
Longtime User
There's a a few threads on the forum discussing how to handle devices with built in 'external' storage.
I don't remember whether they came to any conclusion on the best way to deal with such devices though.

Problems arise when manufacturers use different paths for 'internal' external storage, and apps are hardcoded to use a path that is not compatible with the device.

Currently the OSMDroid cache folder location is hardcoded, there's a recent thread on this forum asking if it could me moved and there a few requests on the official OSMDroid issues page asking if the cache location could be made user definable but the OSMDroid developers have not made any such changes and seem to have no intention of doing so.

Presumably you try to run OSMDroid and it fails because it cannot find writeable external storage?
Do you see any relevant log messages when you try to run OSMDRoid on your Nexus?

I have an Ainol Elf-II tablet that has both internal storage and an SD card slot.
I just removed the SD card and rebooted the device and ran the Simple Map demo, it works fine,using the Elf's internal storage instead of the (removed) SD card.

Can you run this code on you Nexus 4 and post the log:

B4X:
Sub Process_Globals

End Sub

Sub Globals

End Sub

Sub Activity_Create(FirstTime As Boolean)

   Log("File.ExternalWritable="&File.ExternalWritable)
   Log("File.DirRootExternal="&File.DirRootExternal)
   
End Sub

Sub Activity_Resume

End Sub

Sub Activity_Pause (UserClosed As Boolean)

End Sub

Martin.
 

marc2cram

New Member
Licensed User
Longtime User
Thank you very much for this lib warwound. It already helped me a lot....

I am quite a newby with b4a and i im reading (and learning) a lot here in this forum but i havn't found a solution to my problem yet (I do have a workaround for it, but there might be a better solution...)

i am working with the pathoverlay and i want to extract a single point from the list i get with

Dim RPoints As List
RPoints = MyRoutePathesOv(3).GetAllPoints
For i = 0 To RPoints.Size-1
Log("Geopoint " & RPoints.Get(i))
Dim Rpoint As Object
Rpoint = RPoints.Get(i)
Log("Geopoint Type " & GetType(Rpoint))
...
Next

to do something with it...- but - ... i am not getting a GeoPoint object ... I am getting a GeoPointWrapper object. How can i convert this object to a GeoPoint?
Defining Rpoint as GeoPoint in the code shown above creates interesting error messages... ;)

Greetings from Germany...
 

warwound

Expert
Licensed User
Longtime User
OMG not this problem again..!

It's been a problem with no real solution since i created the OSMDroid library.
In your posted code i bet that RPoints is a List of GeoPoint objects and not GeoPointWrapper objects?

How can that be, here's the library source code that creates and returns the List to B4A:

B4X:
   public anywheresoftware.b4a.objects.collections.List getGeoPointWrappers() {
      anywheresoftware.b4a.objects.collections.List geoPointWrappers = new anywheresoftware.b4a.objects.collections.List();
      geoPointWrappers.Initialize();
      for (Point point : this.mOriginalPoints) {
         geoPointWrappers.Add(new GeoPointWrapper(new GeoPoint(point.x, point.y)));
      }
      return geoPointWrappers;
   }

Take a look at this recent thread of mine: http://www.b4x.com/forum/libraries-...71-passing-b4a-list-arraylist.html#post135491

Let's assume that OriginPoint is a GeoPointWrapper object.
GeoPointWrapper uses the B4A AbsObjectWrapper class to wrap the original GeoPoint object so that the GeoPoint object can be used within the B4A IDE.
To complicate things a GeoPointWrapper has a ShortName of GeoPoint - it's ShortName is it's name within the B4A IDE.

Anyway:

B4X:
Dim WayPoints As List
WayPoints.Initialize
WayPoints.Add(OriginPoint)

WayPoints contains a single GeoPoint object, we added a GeopointWrapper but in the compilation process the GeoPointWrapper is unwrapped and becomes the underlying wrapped GeoPoint object.

Similar code:

B4X:
Dim WayPoints As List
WayPoints.Initialize
WayPoints.AddAll(Array As GeoPoint(OriginPoint)

WayPoints contains a single GeoPointWrapper object.
Creating array of GeoPoint objects (GeoPoint here is the IDE ShortName for a GeoPointWrapper NOT the native android GeoPoint object) preserves the original wrapped object.

Knowing this and looking at the above getGeoPointWrappers method source code you can see that the List returned by GetAllPoints will be a List of unwrapped GeoPoint objects and not the desired List of GeoPointWrapper objects.

So what's the best solution without breaking existing code?
  • A new method GetAllPointsAsArray which would return an Array of GeoPointWrappers.
  • A new method GetPoint(PointIndex) which would return a single GeoPointWrapper.
  • A new GeoPoint Initialize method which would accept an unwrapped GeoPoint as a parameter and return a GeoPointWrapper - the GeoPointWrapper being the passed geopoint now wrapped:
    B4X:
    Dim GeoPoint1 As GeoPoint
    GeoPoint1.NewInitializeMethod(RawGeoPoint)
    ' GeoPoint1 will now be RawGeoPoint wrapped as a GeoPointWrapper so you can work with it in B4A
All three solutions would work for you.

To be honest the existing OSMDroid library has become very bloated with bug fixes and deprecated objects, i can patch it up so that it'll work for your project but my main aim is to continue to work on the new version mentioned here.
I hope the new version will be free of all of these little technical bugs and snags and deprecated objects and be much easier to use.

Meanwhile take a look at the three solutions i've offered and let me know which one you'd find most useful.

Martin.
 

marc2cram

New Member
Licensed User
Longtime User
:signOops: sorry, i might have found one of those...

Don't worry, i have my workaround (which uses the original list of GeoPoints which was assigned to the overlay...); this is ok for me.

Thank you very much - and the lib is really very good.


...to answer your question about the solution:
the issue i want to solve is to find the point with the min distance to a reference point (I just want to click on the map to find the nearest point on the path) - this normally needs the whole list of points... this points towards the array... (or even a "minDistance" method ;) )
 
Last edited:

drgottjr

Expert
Licensed User
Longtime User
There's a a few threads on the forum discussing how to handle devices with built in 'external' storage.
I don't remember whether they came to any conclusion on the best way to deal with such devices though.

Problems arise when manufacturers use different paths for 'internal' external storage, and apps are hardcoded to use a path that is not compatible with the device.

Currently the OSMDroid cache folder location is hardcoded, there's a recent thread on this forum asking if it could me moved and there a few requests on the official OSMDroid issues page asking if the cache location could be made user definable but the OSMDroid developers have not made any such changes and seem to have no intention of doing so.

Presumably you try to run OSMDroid and it fails because it cannot find writeable external storage?
Do you see any relevant log messages when you try to run OSMDRoid on your Nexus?

I have an Ainol Elf-II tablet that has both internal storage and an SD card slot.
I just removed the SD card and rebooted the device and ran the Simple Map demo, it works fine,using the Elf's internal storage instead of the (removed) SD card.

Can you run this code on you Nexus 4 and post the log:

B4X:
Sub Activity_Create(FirstTime As Boolean)
   Log("File.ExternalWritable="&File.ExternalWritable)
   Log("File.DirRootExternal="&File.DirRootExternal)
End Sub
Martin.


sorry for the delay in responding. just to close this threadlette, i ran your code and got: true and /storage/emulated/0, respectively. the second value might be of interest for some.

so osmdroid is fine. what i incorrectly interpreted as "quietly" failing, was simply "slowly" running. in the simplemap test of osmdroid, you see a blank grid on startup. i thought it might just be the emulator, so i deployed it to the device and got the same result. that's when i put out the call. when i saw that your test code showed that osmdroid should work, i tried again (on the emulator). same blank grid for at least 20 seconds. i was about to give up, but this time i was called away, and when i got back to my computer, the tiles had begun to appear! i presume if i waited long enough on the device, i would get the same results. i apologize for the false alarm.
 

warwound

Expert
Licensed User
Longtime User
The problem you're experiencing is caused by the tile servers that OSMDroid uses.
They are all free for anyone to use, and they are generally funded by donations and run by volunteers.
They have limited financial and technical resources - they get slow and overloaded.
And as more and more mapping applications make use of these free tile servers they get even slower and more overloaded.

A solution might be to try CloudMade, they offer a free service and you can even customise the tiles.
CloudMade have more powerful servers and you might find their tiles loaded (much) faster than the other free tile servers.

More info on using CloudMade tiles in OSMDroid can be found here: http://www.b4x.com/forum/basic4andr...smdroid-mapview-b4a-tutorial-2.html#post92864

Martin.
 
Top