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,804
Last edited:

warwound

Expert
Licensed User
Longtime User
AIM: Save and restore the MapView center and zoom level on device orientation change.

B4X:
Sub Process_Globals
   '   create a process global for each map state to be saved and restored on orientation change
   
   '   use a GeoPoint object to save the map center
   Dim MapCenter As GeoPoint
   
   '   an Int is all that's required to save the map zoom level
   Dim ZoomLevel As Int
End Sub

Sub Globals
   Dim MapView1 As MapView
End Sub

Sub Activity_Create(FirstTime As Boolean)
   MapView1.Initialize("")
   Activity.AddView(MapView1, 0, 0, 100%x, 100%y)
   MapView1.SetZoomEnabled(True)
   MapView1.SetMultiTouchEnabled(True)
   
   If FirstTime Then
      '   set the default initial map center and zoom level
      MapCenter.Initialize(52.75192, 0.40505)
      ZoomLevel=14
   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
   MapCenter=MapView1.GetCenter
   ZoomLevel=MapView1.Zoom
End Sub

Again the code is pretty self-explanatory.

Two process global variables are used to save the map center and zoom level in Activity_Pause.

In Activity_Create we check whether this is the first time that the activity has been created and if so set a default zoom level and map center.
The MapView zoom level and center are then set - setting the initial default state or restoring the last saved state.

I have used a GeoPoint object to save the map center as this is the type of object returned by the MapView GetCenter method.

Pan and zoom the map and rotate your device and you'll see the map retains it's previous zoom level and center.

Martin.
 

Attachments

  • 02 - SimpleMap.zip
    5.7 KB · Views: 2,179

warwound

Expert
Licensed User
Longtime User
AIM: Change the MapView TileSource, save and restore the selected TileSource on device orientation change.

By default the MapView displays a tile set provided by Mapnik:

Mapnik.png


The MapView has other built in TileSource providers (and you can even add your own), you can get a List of all available TileSources by calling the MapView GetTileSources method.
GetTileSources returns a List of Strings, each String item can be used in the MapView SetTileSource method to select that TileSource.

Passing an invalid String to SetTileSource will result in an IllegalArgumentException.

So what Strings are valid TileSources?
This is what GetTileSources returns for the current version of OSMDroid: "Mapnik", "CycleMap", "OSMPublicTransport", "Base", "Topo", "Hills", "CloudMadeStandardTiles", "CloudMadeSmallTiles", "MapquestOSM", "MapquestAerial" and "MapsForFree".

Only those Strings are valid to be used with SetTileSource.
BUT not all of those TileSources will actually display tiles.
Some require a license and/or an API key from the TileSource provider.

What TileSources are will actually display tiles?

Mapnik i mentioned is the default TileSource and obviously displays tiles.

Currently the other TileSources that i've found to display tiles are: "CycleMap", "OSMPublicTransport", "MapquestOSM", "MapquestAerial" and "MapsForFree".

So you have a choice of 6 built in TileSources to choose from.
Note that different TileSources have different maximum zoom levels, "MapquestAerial" and "MapsForFree" have particularly low maximum zoom levels.
That is unless you are looking at the USA, "MapquestAerial" has much higher maximum zoom level available for USA only.

Here's some screenshots of the useable TileSources (not including Mapnik):

CycleMap
CycleMap.png


OSMPublicTransport
OSMPublicTransport.png


MapquestOSM
MapquestOSM.png


MapquestAerial
MapquestAerial.png


MapsForFree
MapsForFree.png


So we now know what TileSources are available and how to set the current TileSource, let's write some code...

B4X:
Sub Process_Globals
   Dim MapCenter As GeoPoint
   Dim ZoomLevel As Int
   
   '   create a new String to save the currently selected TileSource
   Dim TileSource As String
End Sub

Sub Globals
   Dim MapView1 As MapView
   
   '   we'll use a spinner to enable selection of TileSources
   Dim TileSourceSpinner As Spinner
End Sub

Sub Activity_Create(FirstTime As Boolean)
   MapView1.Initialize("")
   
   '   adjust the MapView height to make space for the Spinner
   Activity.AddView(MapView1, 0, 48dip, 100%x, 100%y-48dip)
   
   MapView1.SetZoomEnabled(True)
   MapView1.SetMultiTouchEnabled(True)
   
   If FirstTime Then
      MapCenter.Initialize(52.75192, 0.40505)
      ZoomLevel=14
      
      '   set the default initial TileSource
      TileSource="Mapnik"
   End If
   
   MapView1.Zoom=ZoomLevel
   MapView1.SetCenter3(MapCenter)
   
   TileSourceSpinner.Initialize("TileSourceSelect")
   Activity.AddView(TileSourceSpinner, 0, 0, 100%x, 48dip)
   
   TileSourceSpinner.AddAll(MapView1.GetTileSources)
   TileSourceSpinner.Prompt="Select a TileSource"
   TileSourceSpinner.SelectedIndex=TileSourceSpinner.IndexOf(TileSource)
   '   manually call the Spinner ItemClick Sub to sync the MapView TileSource with the spinner SelectedIndex
   TileSourceSelect_ItemClick(TileSourceSpinner.SelectedIndex, TileSourceSpinner.SelectedItem)
End Sub

Sub Activity_Resume
End Sub

Sub Activity_Pause (UserClosed As Boolean)
   MapCenter=MapView1.GetCenter
   ZoomLevel=MapView1.Zoom
   
   '   save the currently selected MapView TileSource
   TileSource=MapView1.GetTileSource
End Sub

Sub TileSourceSelect_ItemClick (Position As Int, Value As Object)
   '   set the MapView TileSource
   MapView1.SetTileSource(Value)
End Sub

I've used a Spinner to enable the user to select the TileSource.
A new String process global variable is used to save and restore the TileSource on device orientation change.

You'll see that i have added all available TileSources to the Spinner - you can work your way through them to see which ones display tiles and which ones don't.
It may be that some TileSources that have displayed no tiles for me will display tiles when a different part of the world is being viewed - i'm not sure.

So part three of my tutorial is complete and you can add a MapView to your application and save and restore it's state.
You can now also select different TileSources to display.

What's next?

I'll cover the various Overlay layers next...

Martin.
 

Attachments

  • 03 - SimpleMap.zip
    5.9 KB · Views: 2,443

warwound

Expert
Licensed User
Longtime User
AIM: Add a ScaleBarOverlay layer to the MapView.

The MapView has various Overlay layers available:

  • MarkersFocusOverlay
  • MarkersOverlay
  • MinimapOverlay
  • MyLocationOverlay
  • PathOverlay
  • ScaleBarOverlay
  • SimpleLocationOverlay

I shall start with the simple layers and progress to the more advanced layers, first example then is the ScaleBarOverlay.

ScaleBarOverlay displays a scalebar on the map, the scale adjusts as you zoom in and out.

Some example code:

B4X:
Sub Process_Globals
   Dim MapCenter As GeoPoint
   Dim TileSource As String
   Dim ZoomLevel As Int
End Sub

Sub Globals
   Dim MapView1 As MapView
   Dim TileSourceSpinner As Spinner
   
   '   create a ScaleBarOverlay
   Dim ScaleBarOverlay1 As ScaleBarOverlay
End Sub

Sub Activity_Create(FirstTime As Boolean)
   MapView1.Initialize("")
   Activity.AddView(MapView1, 0, 48dip, 100%x, 100%y-48dip)
   
   MapView1.SetMultiTouchEnabled(True)
   MapView1.SetZoomEnabled(True)
   
   If FirstTime Then
      MapCenter.Initialize(52.75192, 0.40505)
      TileSource="Mapnik"
      ZoomLevel=14
   End If
   
   MapView1.Zoom=ZoomLevel
   MapView1.SetCenter3(MapCenter)
   
   TileSourceSpinner.Initialize("TileSourceSelect")
   Activity.AddView(TileSourceSpinner, 0, 0, 100%x, 48dip)
   
   TileSourceSpinner.AddAll(MapView1.GetTileSources)
   TileSourceSpinner.Prompt="Select a TileSource"
   TileSourceSpinner.SelectedIndex=TileSourceSpinner.IndexOf(TileSource)
   TileSourceSelect_ItemClick(TileSourceSpinner.SelectedIndex, TileSourceSpinner.SelectedItem)
   
   '   i'll add an activity menuitem so we can change various properties at run-time
   Activity.AddMenuItem("Do something", "DoSomething")
   
   '   initialize the ScaleBarOverlay and add it to the MapView
   ScaleBarOverlay1.Initialize(MapView1)
   MapView1.AddOverlay(ScaleBarOverlay1)
   
End Sub

Sub Activity_Resume
End Sub

Sub Activity_Pause (UserClosed As Boolean)
   MapCenter=MapView1.GetCenter
   TileSource=MapView1.GetTileSource
   ZoomLevel=MapView1.Zoom
End Sub

Sub DoSomething_Click
   '   change some ScaleBarOverlay defaults
   ScaleBarOverlay1.SetLineColor(Colors.Red)
   ScaleBarOverlay1.SetLineWidth(4)
   ScaleBarOverlay1.SetOffset(10, 128)
   ScaleBarOverlay1.SetScaleUnit(ScaleBarOverlay1.SCALE_UNIT_IMPERIAL)
   ScaleBarOverlay1.SetTextColor(Colors.Green)
   ScaleBarOverlay1.SetTextSize(24)
   '   it is required that the MapView Invalidate method is called to force the MapView to update it's display
   MapView1.Invalidate
End Sub

Sub TileSourceSelect_ItemClick (Position As Int, Value As Object)
   MapView1.SetTileSource(Value)
End Sub

The ScaleBarOverlay is initialized and added to the MapView using the AddOverlay method.
All ScaleBarOverlay properties are left at their default values, the ScaleBarOverlay displays metric units and is positioned at (10, 10) within the MapView.

I've added a MenuItem and, on click, that calls a Sub DoSomething_Click where i change some ScaleBarOverlay default values and then call the MapView Invalidate method to force the MapView to update it's display.

A click on the MenuItem changes the scale bar line width and color, offset position, scale unit, text color and text size.

The next Overlay that i'll cover is the MinimapOverlay...

Martin.
 

Attachments

  • 04 - OverlayMap.zip
    6.1 KB · Views: 2,028

warwound

Expert
Licensed User
Longtime User
AIM: Add a MinimapOverlay layer to the MapView.


The MinimapOverlay displays a small map in the bottom right hand corner of the MapView.
It is an overview map - zoomed out more than the main MapView map to give an overview of the area being viewed.

You can drag the MinimapOverlay and the main map will also be panned.
The MinimapOverlay does not support double tap to zoom.

Here's the example code:

B4X:
Sub Process_Globals
   Dim MapCenter As GeoPoint
   Dim TileSource As String
   Dim ZoomLevel As Int
End Sub

Sub Globals
   Dim MapView1 As MapView
   Dim ScaleBarOverlay1 As ScaleBarOverlay
   Dim TileSourceSpinner As Spinner
   
   '   create the MinimapOverlay
   Dim MinimapOverlay1 As MinimapOverlay
End Sub

Sub Activity_Create(FirstTime As Boolean)
   Activity.AddMenuItem("Do something", "DoSomething")
   
   MapView1.Initialize("")
   Activity.AddView(MapView1, 0, 48dip, 100%x, 100%y-48dip)
   
   MapView1.SetMultiTouchEnabled(True)
   MapView1.SetZoomEnabled(True)
   
   If FirstTime Then
      MapCenter.Initialize(52.75192, 0.40505)
      TileSource="Mapnik"
      ZoomLevel=14
   End If
   
   MapView1.Zoom=ZoomLevel
   MapView1.SetCenter3(MapCenter)
   
   ScaleBarOverlay1.Initialize(MapView1)
   MapView1.AddOverlay(ScaleBarOverlay1)
   
   '   initialize the MinimapOverlay and add it to the MapView
   '   ** IMPORTANT a MinimapOverlay must be the LAST overlay layer to be added to a MapView otherwise other overlay layers may not display correctly **
   '   This may be fixed with future versions of the native Android OSMDroid library.
   MinimapOverlay1.Initialize(MapView1)
   MapView1.AddOverlay(MinimapOverlay1)
   
   
   '   moved the spinner initialization to here so that when TileSourceSelect_ItemClick is called the MinimapOverlay has been initialized and it's TileSource will be set (as well as the MapView TileSource being set)
   TileSourceSpinner.Initialize("TileSourceSelect")
   Activity.AddView(TileSourceSpinner, 0, 0, 100%x, 48dip)
   
   TileSourceSpinner.AddAll(MapView1.GetTileSources)
   TileSourceSpinner.Prompt="Select a TileSource"
   TileSourceSpinner.SelectedIndex=TileSourceSpinner.IndexOf(TileSource)
   TileSourceSelect_ItemClick(TileSourceSpinner.SelectedIndex, TileSourceSpinner.SelectedItem)
End Sub

Sub Activity_Resume
End Sub

Sub Activity_Pause (UserClosed As Boolean)
   MapCenter=MapView1.GetCenter
   TileSource=MapView1.GetTileSource
   ZoomLevel=MapView1.Zoom
End Sub

Sub DoSomething_Click
   '   change some MinimapOverlay defaults
   MinimapOverlay1.Height=150
   MinimapOverlay1.Padding=50
   MinimapOverlay1.Width=75
   '   it is required that the MapView Invalidate method is called to force the MapView to update it's display
   MapView1.Invalidate
End Sub

Sub TileSourceSelect_ItemClick (Position As Int, Value As Object)
   MapView1.SetTileSource(Value)
   
   '   set the MinimapOverlay TileSource to match the main MapView map TileSource
   MinimapOverlay1.SetTileSource(Value)
End Sub

The MinimapOverlay is initialized and added to the MapView using the AddOverlay method.
By default the MinimapOverlay is 100 pixels square and padded by 10 pixels from the bottom right hand corner of the Mapview.

IMPORTANT a MinimapOverlay must be the LAST overlay layer to be added to a MapView otherwise other overlay layers may not display correctly.
This may be fixed with future versions of the native Android OSMDroid library.


I've updated the Sub TileSourceSelect_ItemClick so that it sets the MinimapOverlay TileSource as well as the main MapView TileSource.
That required that the Spinner and the call to TileSourceSelect_ItemClick(TileSourceSpinner.SelectedIndex, TileSourceSpinner.SelectedItem) be moved in the code to after the MinimapOverlay has been initialized.

A click on the MenuItem now changes the width, height and padding of the MinimapOverlay.

Pan and zoom the map, change the TileSource and change the device orientation.
The MinimapOverlay TileSource is now restored along with the main MapView TileSource.

I shall cover the SimpleLocationOverlay layer next.

Martin.
 

Attachments

  • 05 - OverlayMap.zip
    6.2 KB · Views: 1,822

warwound

Expert
Licensed User
Longtime User
AIM: Add a SimpleLocationOverlay layer to the MapView.

The SimpleLocationOverlay displays a single placemark(marker) on the map with a default person icon.

It allows you to to change the location of the placemark at runtime.
But you cannot change the default icon.

The SimpleLocationOverlay requires the resources in the drawable-nodpi folder that is included with the library download.
Copy the drawable-nodpi to your project's Objects/res folder and ensure that all files in that folder are set to read-only


Here's the example code:

B4X:
Sub Process_Globals
   Dim MapCenter As GeoPoint
   Dim TileSource As String
   Dim ZoomLevel As Int
End Sub

Sub Globals
   Dim MapView1 As MapView
   Dim MinimapOverlay1 As MinimapOverlay
   Dim ScaleBarOverlay1 As ScaleBarOverlay
   Dim TileSourceSpinner As Spinner
   
   '   create the SimpleLocationOverlay
   Dim SimpleLocationOverlay1 As SimpleLocationOverlay
End Sub

Sub Activity_Create(FirstTime As Boolean)
   Activity.AddMenuItem("Do something", "DoSomething")
   
   '   pass an EventName when initializing the MapView
   '   MapView has two events: CenterChanged and ZoomChanged
   MapView1.Initialize("MapView1")
   Activity.AddView(MapView1, 0, 48dip, 100%x, 100%y-48dip)
   
   MapView1.SetMultiTouchEnabled(True)
   MapView1.SetZoomEnabled(True)
   
   If FirstTime Then
      MapCenter.Initialize(52.75192, 0.40505)
      TileSource="Mapnik"
      ZoomLevel=14
   End If
   
   MapView1.Zoom=ZoomLevel
   MapView1.SetCenter3(MapCenter)
   
   ScaleBarOverlay1.Initialize(MapView1)
   MapView1.AddOverlay(ScaleBarOverlay1)
   
   
   '   initialize the SimpleLocationOverlay and add it to the MapView
   SimpleLocationOverlay1.Initialize(MapView1)
   MapView1.AddOverlay(SimpleLocationOverlay1)
   
   '   set the SimpleLocationOverlay to display it's icon at the map center
   SimpleLocationOverlay1.SetMyLocation3(MapView1.GetCenter)
   '   note that there is no need to call the MapView Invalidate method here as that gets called (internally) when the MinimapOverlay is added to the MapView next
   
   '   ensure that the MinimapOverlay is the LAST overlay added to the MapView
   MinimapOverlay1.Initialize(MapView1)
   MapView1.AddOverlay(MinimapOverlay1)
   
   TileSourceSpinner.Initialize("TileSourceSelect")
   Activity.AddView(TileSourceSpinner, 0, 0, 100%x, 48dip)
   
   TileSourceSpinner.AddAll(MapView1.GetTileSources)
   TileSourceSpinner.Prompt="Select a TileSource"
   TileSourceSpinner.SelectedIndex=TileSourceSpinner.IndexOf(TileSource)
   TileSourceSelect_ItemClick(TileSourceSpinner.SelectedIndex, TileSourceSpinner.SelectedItem)
End Sub

Sub Activity_Resume
End Sub

Sub Activity_Pause (UserClosed As Boolean)
   MapCenter=MapView1.GetCenter
   TileSource=MapView1.GetTileSource
   ZoomLevel=MapView1.Zoom
End Sub

Sub DoSomething_Click
   '   set the map center to it's default location
   MapView1.SetCenter(52.75192, 0.40505)
   '   it is NOT required that the MapView Invalidate method is called
   '   changes to an Overlay require Invalidate but not changes to the MapView
   '   MapView1.Invalidate
End Sub

'   listen for the MapView CenterChanged event
Sub MapView1_CenterChanged
   Log("MapView1_CenterChanged")
   '   keep the SimpleLocationOverlay centered on the map center
   '   MapView.Invalidate is not called here as the MapView will (internally) invalidate itself when it's center changes
   SimpleLocationOverlay1.SetMyLocation3(MapView1.GetCenter)
End Sub

'   listen for the MapView ZoomChanged event
Sub MapView1_ZoomChanged
   Log("MapView1_ZoomChanged ZoomLevel="&MapView1.Zoom)
End Sub

Sub TileSourceSelect_ItemClick (Position As Int, Value As Object)
   MapView1.SetTileSource(Value)
   MinimapOverlay1.SetTileSource(Value)
End Sub

I'm now initializing the MapView with an EventName.
A MapView can generate two events: 'CenterChanged' and 'ZoomChanged'.
So i've created Subs to listen for those events.

The SimpleLocationOverlay is initialized and added to the MapView, it's icon set to display at the map center.

When the MapView CenterChanged event is triggered, the SimpleLocationOverlay is set to display at the new map center.
That keeps the SimpleLocationOverlay centered on the map center at all times.

A click on the MenuItem sets the map center to it's default location - which causes a CenterChanged event and the SimpleLocationOverlay is again centered on the map center.

The Sub MapView1_ZoomChanged does nothing except log that it has been called.

I shall cover the PathOverlay next.

Martin.
 

Attachments

  • 06 - OverlayMapSimpleLocation.zip
    38.5 KB · Views: 2,163
Last edited:

warwound

Expert
Licensed User
Longtime User
AIM: Add a PathOverlay layer to the MapView.

A PathOverlay enables you to display polylines and polygons in a MapView.

Here's the example code, instead of starting with the code from the end of the SimpleLocationOverlay example i am starting with the code from the end of the MinimapOverlay example.
(I'll do this with the examples for all of the more complex Overlays layers so that the example code is easier to follow).

The code:

B4X:
Sub Process_Globals
   Dim MapCenter As GeoPoint
   Dim TileSource As String
   Dim ZoomLevel As Int
   
   '   create a List to store the path points in Activity_Pause
   Dim PathPoints As List
End Sub

Sub Globals
   Dim MapView1 As MapView
   Dim MinimapOverlay1 As MinimapOverlay
   Dim ScaleBarOverlay1 As ScaleBarOverlay
   Dim TileSourceSpinner As Spinner
   
   '   create the PathOverlay
   Dim PathOverlay1 As PathOverlay
End Sub

Sub Activity_Create(FirstTime As Boolean)
   Activity.AddMenuItem("Do something", "DoSomething")
   
   MapView1.Initialize("MapView1")
   Activity.AddView(MapView1, 0, 48dip, 100%x, 100%y-48dip)
   
   MapView1.SetMultiTouchEnabled(True)
   MapView1.SetZoomEnabled(True)
   
   If FirstTime Then
      MapCenter.Initialize(52.75192, 0.40505)
      TileSource="Mapnik"
      ZoomLevel=12
      
      '   initialize the PathPoints List
      PathPoints.Initialize
   End If
   
   MapView1.Zoom=ZoomLevel
   MapView1.SetCenter3(MapCenter)
   
   ScaleBarOverlay1.Initialize(MapView1)
   MapView1.AddOverlay(ScaleBarOverlay1)
   
   '   initialize the PathOverlay and add it to the MapView, the path will be colored red
   PathOverlay1.Initialize(MapView1, Colors.Red)
   MapView1.AddOverlay(PathOverlay1)
   
   '   change the path StrokeWidth to 4 pixels (by default it is 2 pixels)
   PathOverlay1.StrokeWidth=4
   
   '   make the path 50% transparent (by default it is fully opaque)
   PathOverlay1.Alpha=128
   
   If FirstTime Then
      '   make the map center the first point on the path
      PathOverlay1.AddPoint3(MapCenter)
      '   no need to call MapView Invalidate - it will be called (internally) when the next Overlay (MinimapOverlay) is added to the MapView
   Else
      '   restore the path points
      PathOverlay1.AddPoints(PathPoints)
      
      '   clear the PathPoints List as it's no longer required
      PathPoints.Clear
   End If
   
   
   '   ensure that the MinimapOverlay is the LAST overlay added to the MapView
   MinimapOverlay1.Initialize(MapView1)
   MapView1.AddOverlay(MinimapOverlay1)
   
   TileSourceSpinner.Initialize("TileSourceSelect")
   Activity.AddView(TileSourceSpinner, 0, 0, 100%x, 48dip)
   
   TileSourceSpinner.AddAll(MapView1.GetTileSources)
   TileSourceSpinner.Prompt="Select a TileSource"
   TileSourceSpinner.SelectedIndex=TileSourceSpinner.IndexOf(TileSource)
   TileSourceSelect_ItemClick(TileSourceSpinner.SelectedIndex, TileSourceSpinner.SelectedItem)
End Sub

Sub Activity_Resume
End Sub

Sub Activity_Pause (UserClosed As Boolean)
   MapCenter=MapView1.GetCenter
   TileSource=MapView1.GetTileSource
   ZoomLevel=MapView1.Zoom
   
   '   save the PathOverlay1 points
   PathPoints=PathOverlay1.GetAllPoints
End Sub

Sub DoSomething_Click
   Log("PathOverlay1 contains "&PathOverlay1.GetNumberOfPoints&" points")
   '   set the map center and zoom level so that it contains the entire path
   MapView1.FitMapToBoundingBox(PathOverlay1.GetBoundingBox)
   '   no need to call MapView Invalidate - the FitMapToBoundingBox methods calls this internally
   
   '   note that by recentering the map the path will extend from it's last point to the new map center
   
   '   MapView1.Invalidate
End Sub

Sub MapView1_CenterChanged
   Log("MapView1_CenterChanged")
   '   add the new map center to the path
   PathOverlay1.AddPoint3(MapView1.GetCenter)
End Sub

Sub MapView1_ZoomChanged
   Log("MapView1_ZoomChanged")
End Sub

Sub TileSourceSelect_ItemClick (Position As Int, Value As Object)
   MapView1.SetTileSource(Value)
   MinimapOverlay1.SetTileSource(Value)
End Sub

First i've created a List PathPoints which will be used to save and restore the path on orientation change.

Then i create, initialize and add the PathOverlay to the MapView.
The PathOverlay is initialized to display the path with color red.
I override some PathOverlay defaults - change the stroke width to 4 pixels and the alpha transparency to 50% transparent.

If this is the first time that the activity has been created then the map center is added to the path as it's first point, otherwise the previously saved point (PathPoints List) is used to set the points of the path.
PathPoints List is cleared to reclaim any used memory - it's not going to be read again so there's no need to keep it populated.

In Activity_Pause all the PathOverlay points are saved to Pathpoints List.

Finally in the MapView CenterChanged event listener i add the new map center to the path.

Start the example and drag the map, the path starts as a single point at the map center and follows the map center as you drag it.

A click on the MenuItem demonstrates the use of two new methods.
First the PathOverlay GetBoundingBox method is used.
This returns a BoundingBox object that represents the area covered by the path.
Then the MapView FitMapToBoundingBox is called passing the PathOverlay BoundingBox.
The map will adjust it's center and zoom level tofully contain the path.

Change the orientation of you device and you'll see the already drawn path is saved and restored.

I'll cover the MyLocationOverlay next.

Martin.
 

Attachments

  • 07 - OverlayMap.zip
    6.4 KB · Views: 1,973

warwound

Expert
Licensed User
Longtime User
AIM: Add a MyLocationOverlay layer to the MapView.

MyLocationOverlay has quite a lot of built in functionality and should prove useful to many developers.

It provides GPS related features that require little or no additional code to implement.

The MyLocationOverlay requires the resources in the drawable-nodpi folder that is included with the library download.
Copy the drawable-nodpi to your project's Objects/res folder and ensure that all files in that folder are set to read-only


Here's the example code:

B4X:
Sub Process_Globals
   Dim MapCenter As GeoPoint
   Dim TileSource As String
   Dim ZoomLevel As Int
   
   '   create some variables to save the MyLocationOverlay state
   Dim CompassEnabled, FollowLocationEnabled, MyLocationEnabled As Boolean
End Sub

Sub Globals
   Dim MapView1 As MapView
   Dim MinimapOverlay1 As MinimapOverlay
   Dim ScaleBarOverlay1 As ScaleBarOverlay
   Dim TileSourceSpinner As Spinner
   
   '   create the MyLocationOverlay
   Dim MyLocationOverlay1 As MyLocationOverlay
End Sub

Sub Activity_Create(FirstTime As Boolean)
   '   update the MenuItems
   Activity.AddMenuItem("Toggle Compass", "MenuItemSelect")
   Activity.AddMenuItem("Toggle MyLocation", "MenuItemSelect")
   Activity.AddMenuItem("Toggle FollowLocation", "MenuItemSelect")
   
   '   MapView initialized with no EventName as we'll not be listening for MapView events
   MapView1.Initialize("")
   Activity.AddView(MapView1, 0, 48dip, 100%x, 100%y-48dip)
   
   MapView1.SetMultiTouchEnabled(True)
   MapView1.SetZoomEnabled(True)
   
   '   initialize and add the MyLocationOverlay to the MapView, an EventName is used as we'll be listening for all three events that this overlay generates
   MyLocationOverlay1.Initialize(MapView1, "MyLocationOverlay1")
   MapView1.AddOverlay(MyLocationOverlay1)
   
   If FirstTime Then
      MapCenter.Initialize(52.75192, 0.40505)
      TileSource="Mapnik"
      ZoomLevel=14
      
      '   set the default values for the new variables
      CompassEnabled=MyLocationOverlay1.CompassEnabled
      FollowLocationEnabled=MyLocationOverlay1.FollowLocationEnabled
      MyLocationEnabled=MyLocationOverlay1.MyLocationEnabled
   Else
      '   restore the MyLocationOverlay state
      MyLocationOverlay1.CompassEnabled=CompassEnabled
      MyLocationOverlay1.FollowLocationEnabled=FollowLocationEnabled
      MyLocationOverlay1.MyLocationEnabled=MyLocationEnabled
   End If
   
   MapView1.Zoom=ZoomLevel
   MapView1.SetCenter3(MapCenter)
   
   ScaleBarOverlay1.Initialize(MapView1)
   MapView1.AddOverlay(ScaleBarOverlay1)
   
   '   ensure that the MinimapOverlay is the LAST overlay added to the MapView
   MinimapOverlay1.Initialize(MapView1)
   MapView1.AddOverlay(MinimapOverlay1)
   
   TileSourceSpinner.Initialize("TileSourceSelect")
   Activity.AddView(TileSourceSpinner, 0, 0, 100%x, 48dip)
   
   TileSourceSpinner.AddAll(MapView1.GetTileSources)
   TileSourceSpinner.Prompt="Select a TileSource"
   TileSourceSpinner.SelectedIndex=TileSourceSpinner.IndexOf(TileSource)
   TileSourceSelect_ItemClick(TileSourceSpinner.SelectedIndex, TileSourceSpinner.SelectedItem)
End Sub

Sub Activity_Resume
End Sub

Sub Activity_Pause (UserClosed As Boolean)
   MapCenter=MapView1.GetCenter
   TileSource=MapView1.GetTileSource
   ZoomLevel=MapView1.Zoom
   
   '   save the MyLocationOverlay state
   CompassEnabled=MyLocationOverlay1.CompassEnabled
   FollowLocationEnabled=MyLocationOverlay1.FollowLocationEnabled
   MyLocationEnabled=MyLocationOverlay1.MyLocationEnabled
   
   '   disable MyLocationOverlay compass and GPS listening
   MyLocationOverlay1.CompassEnabled=False
   MyLocationOverlay1.MyLocationEnabled=False
End Sub

Sub MenuItemSelect_Click
   Dim MenuItem As String
   MenuItem=Sender
   Select MenuItem
      Case "Toggle Compass"
         MyLocationOverlay1.CompassEnabled=Not(MyLocationOverlay1.CompassEnabled)
      Case "Toggle MyLocation"
         MyLocationOverlay1.MyLocationEnabled=Not(MyLocationOverlay1.MyLocationEnabled)
      Case "Toggle FollowLocation"
         MyLocationOverlay1.FollowLocationEnabled=Not(MyLocationOverlay1.FollowLocationEnabled)
         If MyLocationOverlay1.MyLocationEnabled=False Then
            '   FollowLocation will only function if MyLocation is ALSO enabled
            ToastMessageShow("FollowLocation will only function if MyLocation is ALSO enabled", True)
         End If
   End Select
End Sub

'   event listeners for the three events that MyLocationOverlay generates

Sub MyLocationOverlay1_FollowLocationAutoDisabled
   Log("MyLocationOverlay1_FollowLocationAutoDisabled")
   '   if FollowLocation is enabled and the map is dragged then FollowLocation is automatically disabled
   ToastMessageShow("FollowLocation is now DISABLED due to the map being dragged", False)
End Sub

Sub MyLocationOverlay1_LocationChanged(NewLocation As Location, IsFirstFix As Boolean)
   Log("MyLocationOverlay1_LocationChanged, Latitude="&NewLocation.Latitude&", Longitude="&NewLocation.Longitude&", IsFirstFix="&IsFirstFix)
End Sub

Sub MyLocationOverlay1_ProviderChanged(Provider As String, Enabled As Boolean)
   '   note that i have yet to see this event generated
   Log("MyLocationOverlay1_ProviderChanged, Provider="&Provider&", Enabled="&Enabled)
End Sub

Sub TileSourceSelect_ItemClick (Position As Int, Value As Object)
   MapView1.SetTileSource(Value)
   MinimapOverlay1.SetTileSource(Value)
End Sub

First i've created three Boolean variables to save the state of the MyLocationOverlay.
The MenuItems have been updated so they make a bit of sense!
Then the MyLocationOverlay is initialized and added to the MapView.

MyLocationOverlay generates three events:

  • FollowLocationAutoDisabled is generated if FollowLocation is enabled and the user drags the map. The drag causes FollowLocation to be automatically disabled.
    This is the default behaviour of the native Android OSMDroid library - i couldn't decide whether to change that default behaviour so have left it as it is for now.
  • LocationChanged is generated each time the MyLocationOverlay gets a new GPS fix.
    The new Location is passed to the event listener as a Location object.
    You must include the B4A GPS library in your project to enable you to work with Location objects.
    A Boolean is also passed to the event listener to indicate whether or not this is the first GPS fix that MyLocationOverlay has obtained.
  • ProviderChanged is generated whenever a location provider is enabled or disabled.
    The event listener is passed a String to identify the location provider and a Boolean to indicate whether or not the provider was enabled or disabled.

These three event listeners do no more than create a log entry, i have included them just for example purposes.
(The FollowLocationAutoDisabled does in fact display a toast notification).

In Activity_Pause the state of the MyLocationOverlay is saved, and the compass and MyLocation listener disabled to prevent unnecessary battery usage of the device sensors.

The Sub MenuItem_Click toggles the state of the MyLocationOverlay compass, MyLocation listener and the FollowLocation property.

Run the example and toggle the compass - you can see the built in compass appear.
Note that i've found this will crash the emulator as soon as the activity tries to enable the non-existent emulator sensors.

Enable MyLocation and you'll probably see nothing on the map - it's centered on Norfolk, UK and you're probably elsewhere!
Enable FollowLocation and the map should pan to show you your location, a circle (the DrawAccuracy circle) is drawn and it's radius indicates the accuracy of the last GPS fix.

If you disable MyLocation after it has found at least one GPS fix then the person icon will still remain drawn on the map.

Rotate your device after MyLocation has found at least one GPS fix and the person icon is being displayed - when the activity is recreated MyLocation will be restored to enabled but the person icon will not be drawn until a new fresh first GPS fix is found.

Disabling MyLocation does not mean that the last fix will no longer be displayed- you would have to use the Mapview RemoveOverlay method to do that:

B4X:
MapView1.RemoveOverlay(MyLocationOverlay1)

So that's the MyLocationOverlay covered.
There are two Overlay layers left to cover: MarkersOverlay and MarkersFocusOverlay.
I shall cover these in the next two examples, starting with MarkersOverlay.

Martin.
 

Attachments

  • 08 - OverlayMapMyLocation.zip
    38.7 KB · Views: 2,306
Last edited:

warwound

Expert
Licensed User
Longtime User
AIM: Add a MarkersOverlay layer to the MapView.

With MarkersOverlay you can add Marker objects to your MapView.

If you use the default Marker icon, then MarkersOverlay requires the resources in the drawable-nodpi folder that is included with the library download.
Copy the drawable-nodpi to your project's Objects/res folder and ensure that all files in that folder are set to read-only


MarkersOverlay generates two events: Click and LongClick.

A Marker object is initialized:

B4X:
Dim Marker1 As Marker
Marker1.Initialize("A title", "A description", aLatitude, aLongitude, anIcon)

When a Marker is Clicked or LongClicked, the title, description and position of the Marker are pass as parameters to the event listener.
The position is passed as a GeoPoint object.

MarkersOverlay leaves the developer to take action when a Marker is Clicked or LongClicked - it does nothing more than generate the events.

Here's the example code:

B4X:
Sub Process_Globals
   Dim MapCenter As GeoPoint
   Dim TileSource As String
   Dim ZoomLevel As Int
End Sub

Sub Globals
   Dim MapView1 As MapView
   Dim MinimapOverlay1 As MinimapOverlay
   Dim ScaleBarOverlay1 As ScaleBarOverlay
   Dim TileSourceSpinner As Spinner
   
   '   create the MarkersOverlay
   Dim MarkersOverlay1 As MarkersOverlay
End Sub

Sub Activity_Create(FirstTime As Boolean)
   '   update the MenuItems
   Activity.AddMenuItem("Fit map to markers", "MenuItemSelect")
   
   '   MapView initialized with no EventName as we'll not be listening for MapView events
   MapView1.Initialize("")
   Activity.AddView(MapView1, 0, 48dip, 100%x, 100%y-48dip)
   
   MapView1.SetMultiTouchEnabled(True)
   MapView1.SetZoomEnabled(True)
   
   '   initialize the MarkersOverlay and add it to the MapView
   '   an EventName is required as we will listen for the two events that MarkersOverlay generates
   MarkersOverlay1.Initialize(MapView1, "MarkersOverlay1")
   MapView1.AddOverlay(MarkersOverlay1)
   
   '   create and initialize 2 Markers
   
   '   for Marker1 i'll use a custom icon
   Dim Icon As BitmapDrawable
   Icon.Initialize(LoadBitmap(File.DirAssets, "my_icon.png"))
   
   Dim Marker1 As Marker
   Marker1.Initialize("Home sweet home", "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi egestas suscipit convallis. Etiam pellentesque gravida est, quis luctus nunc commodo at. Nam et risus orci. Integer malesuada lorem dolor. Maecenas vestibulum cursus enim, tincidunt luctus libero placerat non. In vitae metus tellus, nec euismod nibh. Phasellus ut quam vitae justo sagittis auctor. Sed vel sapien dolor. Etiam ut sem id dolor iaculis ullamcorper. Aenean eget sem nibh, a tempor augue. Nulla interdum luctus molestie.", 52.75610, 0.39748, Icon)
   
   '   Marker2 will display the default OSMDroid icon
   '   the default icon is used if Null is passed as the Icon parameter
   Dim Marker2 As Marker
   Marker2.Initialize("Elsewhere", "Downham Market", 52.60801, 0.39047, Null)
   
   '   create a List and initialize it with the 2 Markers
   Dim Markers As List
   
   '   ** example code updated **
   '   Markers.Initialize2(Array As Marker(Marker1, Marker2))   '   this is the previous code
   Markers.Initialize2(Array As Object(Marker1, Marker2))      '   this is the new code
   
   '   add the List of Markers to the MarkersOverlay
   MarkersOverlay1.AddMarkers(Markers)
   
   If FirstTime Then
      TileSource="Mapnik"
      
      '   fit the MapView to the MarkersOverlay
      MapView1.FitMapToBoundingBox(MarkersOverlay1.GetBoundingBox)
   Else
      '   restore saved zoom level and map center
      MapView1.Zoom=ZoomLevel
      MapView1.SetCenter3(MapCenter)
   End If
   
   
   ScaleBarOverlay1.Initialize(MapView1)
   MapView1.AddOverlay(ScaleBarOverlay1)
   
   '   ensure that the MinimapOverlay is the LAST overlay added to the MapView
   MinimapOverlay1.Initialize(MapView1)
   MapView1.AddOverlay(MinimapOverlay1)
   
   TileSourceSpinner.Initialize("TileSourceSelect")
   Activity.AddView(TileSourceSpinner, 0, 0, 100%x, 48dip)
   
   TileSourceSpinner.AddAll(MapView1.GetTileSources)
   TileSourceSpinner.Prompt="Select a TileSource"
   TileSourceSpinner.SelectedIndex=TileSourceSpinner.IndexOf(TileSource)
   TileSourceSelect_ItemClick(TileSourceSpinner.SelectedIndex, TileSourceSpinner.SelectedItem)
End Sub

Sub Activity_Resume
End Sub

Sub Activity_Pause (UserClosed As Boolean)
   MapCenter=MapView1.GetCenter
   TileSource=MapView1.GetTileSource
   ZoomLevel=MapView1.Zoom
End Sub

'   two event listeners listen for the MarkersOverlay's two events

Sub MarkersOverlay1_Click(Title As String, Description As String, Point As GeoPoint)
   Log("MarkersOverlay1_Click")
   ToastMessageShow(Title&": "&Description, True)
End Sub

Sub MarkersOverlay1_LongClick(Title As String, Description As String, Point As GeoPoint)
   Log("MarkersOverlay1_LongClick")
   ToastMessageShow(Title&": Latitude="&Point.Latitude&", Longitude="&Point.Longitude, True)
   
   '   zoom the map in and center (with animation) on the LongClicked Marker
   MapView1.Zoom=MapView1.GetMaxZoomLevel-1
   MapView1.AnimateTo3(Point)
End Sub

Sub MenuItemSelect_Click
   Dim MenuItem As String
   MenuItem=Sender
   Select MenuItem
      Case "Fit map to markers"
         '   fit the MapView to the MarkersOverlay
         MapView1.FitMapToBoundingBox(MarkersOverlay1.GetBoundingBox)
   End Select
End Sub

Sub TileSourceSelect_ItemClick (Position As Int, Value As Object)
   MapView1.SetTileSource(Value)
   MinimapOverlay1.SetTileSource(Value)
End Sub

A MarkersOverlay is created, initialized and added to the MapView.

Two Markers are created and initialized.
The first Marker is initialized with a custom icon, the second Marker is initialized with Null passed as it's icon so the second Marker will display the default OSMDroid icon.

MarkerOverlay has a single method to add Markers - AddMarkers (Markers As List).
So you must pass this method a List of Marker objects.

If this is the first time that the activity has been created then the MapView FitMapToBoundingBox method is called passing the bounding box returned by the MarkersOverlay GetBoundingBox method.
The map center and zoom level are adjusted to fully display all Markers on the MarkersOverlay.

If this is not the first time that the activity has been created then the previously saved map center and zoom level are restored.
In Activity_Pause the map center and zoom level are saved.

A Click on a Marker will display a toast message with the Marker's Title and Description.

A LongClick on a Marker will display a toast message with the Marker's Title, Latitude and Longitude.
It will also zoom the map in to it's maximum zoom level minus one and center on the Marker's position using the MapView AnimateTo methid.
The animation will likely not be noticeable unless the tiles for the zoomed in area have already been cached.

MarkersOverlay and MarkersFocusOverlay (which i shall cover next) are both functional but lacking in features i think.
You cannot change a Marker's position for example, and if you add Markers and have previously added Markers then the previous Markers are replaced not added to.

Another point to note is how difficult it is to Click or LongClick a small icon.
The default OSMDroid icon seems to small to me, the custom icon i have used is much easier to Click or LongClick.

I shall look at creating a more feature packed MarkersOverlay and MarkersFocusOverlay over the next week or so.

Anyway, on to the last example - the MarkersFocusOverlay.

Martin.
 

Attachments

  • 09 - OverlayMapMarkersOverlay.zip
    40.2 KB · Views: 2,252
Last edited:

warwound

Expert
Licensed User
Longtime User
AIM: Add a MarkersFocusOverlay layer to the MapView.

The MarkersFocusOverlay is very similar to the MarkersOverlay with these differences:

  • MarkersFocusOverlay display a Marker's Title and Desciption in a balloon on the map when the Marker is Clicked..
  • MarkersFocusOverlay generates no events.

MarkersFocusOverlay will be completely re-written as i learn more about the OSMDroid API.
It is basic and functional but not customizable - the balloon background and text color are hard-coded as is the text font size.
The balloon's width is also hard-coded to 200 pixels.
The balloon seems to position itself with a different offset from the Marker depending on whether the Marker uses the default icon or a custom icon.

So i'll show some example code but don't consider this overlay layer to be very useable in an application.

If you use the default Marker icon, then MarkersFocusOverlay requires the resources in the drawable-nodpi folder that is included with the library download.
Copy the drawable-nodpi to your project's Objects/res folder and ensure that all files in that folder are set to read-only


The code:

B4X:
Sub Process_Globals
   Dim MapCenter As GeoPoint
   Dim TileSource As String
   Dim ZoomLevel As Int
End Sub

Sub Globals
   Dim MapView1 As MapView
   Dim MinimapOverlay1 As MinimapOverlay
   Dim ScaleBarOverlay1 As ScaleBarOverlay
   Dim TileSourceSpinner As Spinner
   
   '   create the MarkersOverlay
   Dim MarkersFocusOverlay1 As MarkersFocusOverlay
End Sub

Sub Activity_Create(FirstTime As Boolean)
   '   update the MenuItems
   Activity.AddMenuItem("Fit map to markers", "MenuItemSelect")
   
   '   MapView initialized with no EventName as we'll not be listening for MapView events
   MapView1.Initialize("")
   Activity.AddView(MapView1, 0, 48dip, 100%x, 100%y-48dip)
   
   MapView1.SetMultiTouchEnabled(True)
   MapView1.SetZoomEnabled(True)
   
   '   initialize the MarkersFocusOverlay and add it to the MapView
   MarkersFocusOverlay1.Initialize(MapView1)
   MapView1.AddOverlay(MarkersFocusOverlay1)
   
   '   create and initialize 2 Markers
   
   '   for Marker1 i'll use a custom icon
   Dim Icon As BitmapDrawable
   Icon.Initialize(LoadBitmap(File.DirAssets, "my_icon.png"))
   
   Dim Marker1 As Marker
   Marker1.Initialize("Home sweet home", "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi egestas suscipit convallis. Etiam pellentesque gravida est, quis luctus nunc commodo at. Nam et risus orci. Integer malesuada lorem dolor. Maecenas vestibulum cursus enim, tincidunt luctus libero placerat non. In vitae metus tellus, nec euismod nibh. Phasellus ut quam vitae justo sagittis auctor. Sed vel sapien dolor. Etiam ut sem id dolor iaculis ullamcorper. Aenean eget sem nibh, a tempor augue. Nulla interdum luctus molestie.", 52.75610, 0.39748, Icon)
   
   '   Marker2 will display the default OSMDroid icon
   '   the default icon is used if Null is passed as the Icon parameter
   Dim Marker2 As Marker
   Marker2.Initialize("Elsewhere", "Downham Market", 52.60801, 0.39047, Null)
   
   '   create a List and initialize it with the 2 Markers
   Dim Markers As List
   
   '   ** example code updated **
   '   Markers.Initialize2(Array As Marker(Marker1, Marker2))   '   this is the previous code
   Markers.Initialize2(Array As Object(Marker1, Marker2))      '   this is the new code

   '   add the List of Markers to the MarkersFocusOverlay
   MarkersFocusOverlay1.AddMarkers(Markers)
   
   If FirstTime Then
      TileSource="Mapnik"
      
      '   fit the MapView to the MarkersFocusOverlay
      MapView1.FitMapToBoundingBox(MarkersFocusOverlay1.GetBoundingBox)
   Else
      '   restore saved zoom level and map center
      MapView1.Zoom=ZoomLevel
      MapView1.SetCenter3(MapCenter)
   End If
   
   
   ScaleBarOverlay1.Initialize(MapView1)
   MapView1.AddOverlay(ScaleBarOverlay1)
   
   '   ensure that the MinimapOverlay is the LAST overlay added to the MapView
   MinimapOverlay1.Initialize(MapView1)
   MapView1.AddOverlay(MinimapOverlay1)
   
   TileSourceSpinner.Initialize("TileSourceSelect")
   Activity.AddView(TileSourceSpinner, 0, 0, 100%x, 48dip)
   
   TileSourceSpinner.AddAll(MapView1.GetTileSources)
   TileSourceSpinner.Prompt="Select a TileSource"
   TileSourceSpinner.SelectedIndex=TileSourceSpinner.IndexOf(TileSource)
   TileSourceSelect_ItemClick(TileSourceSpinner.SelectedIndex, TileSourceSpinner.SelectedItem)
End Sub

Sub Activity_Resume
End Sub

Sub Activity_Pause (UserClosed As Boolean)
   MapCenter=MapView1.GetCenter
   TileSource=MapView1.GetTileSource
   ZoomLevel=MapView1.Zoom
End Sub

Sub MenuItemSelect_Click
   Dim MenuItem As String
   MenuItem=Sender
   Select MenuItem
      Case "Fit map to markers"
         '   fit the MapView to the MarkersFocusOverlay
         MapView1.FitMapToBoundingBox(MarkersFocusOverlay1.GetBoundingBox)
   End Select
End Sub

Sub TileSourceSelect_ItemClick (Position As Int, Value As Object)
   MapView1.SetTileSource(Value)
   MinimapOverlay1.SetTileSource(Value)
End Sub

The code is much the same as the MarkersOverlay example code.
MarkersFocusOverlay generates no events so no EventName is required in it's initialization and no event listeners need to be created.

There is only one balloon that displays a Marker's Title and Description, Click a Marker and the balloon will display the Marker's Title and Description, Click another Marker and the balloon will that Marker's Title and Description.
If the balloon is displaying a Marker's Title and Description and you Click that Marker the balloon will toggle to hidden.

So i shall take some time to learn more about the native Android OSMDroid library and hopefully release an update which includes much better MarkersOverlay and MarkersFocusOverlay objects.

Til then hope you all enjoy playing with OSMDroid for B4A!!

Martin.
 

Attachments

  • 10 - OverlayMapMarkersFocusOverlay.zip
    40.3 KB · Views: 2,239
Last edited:

warwound

Expert
Licensed User
Longtime User
OSMDroid has built in support for offline maps.

You can download the tiles for any area, transfer them to the cache folder used by OSMDroid on a device's external memory and view that area without using a data connection.

If a data connection is available and enabled and the user pans the map to an area where there are no offline tiles, then OSMDroid will try and download the required tiles.

If a data connection is NOT available or has been disabled then the map will display a plain background for areas where no offline tiles are available.

Downloading tiles has been made very easy!

All you need is Mobile Atlas Creator.

That link contains the instructions needed, Mobile Atlas Creator will download the tiles you require and package them into a ZIP file ready to be transferred to your device's external memory.
It looks like you can even create a SQLite database to store your offline tiles - i have not tried this yet.

I've used Mobile Atlas Creator to download the tiles which cover my hometown of King's Lynn, Norfolk, UK.
It gave me the option to select an area and then select which zoom levels i wanted to download tiles for.
To keep the offline tile archive file small so i could upload it to the forum i selected just zoom levels 0 to 12.
I've also selected OSM Mapnik as the map tile source.

Downloading tiles for all zoom levels for a large area will create a (very) large offline tile archive file.

In Mobile Atlas Creator, click the Settings button (near Create atlas button) and select the Tile update tab.

Tiles have an expiry date, if your offline tiles pass their expiry date then they should not be used and OSMDroid should download fresh tiles.
I've not tested this yet so it may be that if no data connection is available then expired tiles will be displayed.
I shall experiment and post my findings.

Anyway lets leave the Tile update Settings tab as it is by default.

The Directories tab let you select whereabouts your offline tile archive will be saved.

So having selected a map tile source, an area of the map and finally the zoom levels for which you want to download tiles you just click Create atlas and wait for it to produce your offline tile archive.

My offline tile archive is just 316KBs in size.

If the created offline tile cache has a filename that uses one or more uppercase letters then rename it - we will include this file in the project assets and only all lowercase filenames are allowed.

Transfer your offline file cache to your project's Files folder, remember to add it to your project using the Files tab Add files button so that it is included in the APK.

Now before i go any further consider what tiles are already cached on your device's external storage...
I've got many Mapnik tiles cached on my SD card - for this example i want to be sure that my offline tiles are used and not previously cached tiles.

OSMDroid caches tiles to folder on your device's external storage named osmdroid.
It's shared by all (installed) applications that use an OSMDroid based map - whether using this library or the native Android library.

Inside the cache folder is a folder named tiles and inside that are folders named after the tilesource (Mapnik for example).
Each tilesource folder contains all cached tiles for that tilesource named and structured according to the Slippy map tile naming convention.

So i've used a file manager to rename my Mapnik tilecache from Mapnik to Mapnikbak.

Now i can test my offline files:

B4X:
Sub Process_Globals
   Dim MapCenter As GeoPoint
   Dim ZoomLevel As Int
End Sub

Sub Globals
   Dim MapView1 As MapView
End Sub

Sub Activity_Create(FirstTime As Boolean)
   If FirstTime Then
      Dim OfflineTileCacheFilename, TileCacheDir As String
      OfflineTileCacheFilename="offline_tile_cache_mapnik.zip"
      TileCacheDir=File.DirRootExternal&"/osmdroid"
      '   check if the offline tile cache has already been copied to the OSMDroid cache folder
      If File.Exists(TileCacheDir, OfflineTileCacheFilename)=False Then
         '   create the 'osmdroid' cache folder if it does not exist
         If File.Exists(TileCacheDir&"/", "")=False Then
            File.MakeDir(File.DirRootExternal, "osmdroid")
         End If
         '   copy the offline tile cache to the OSMDroid cache folder
         File.Copy(File.DirAssets, OfflineTileCacheFilename, TileCacheDir, OfflineTileCacheFilename)
      End If
   End If
   
   MapView1.Initialize("MapView1")
   Activity.AddView(MapView1, 0, 0, 100%x, 100%y)
   
   MapView1.SetMultiTouchEnabled(True)
   MapView1.SetZoomEnabled(True)
   
   '   ensure that the map displays the Mapnik tiles
   MapView1.SetTileSource("Mapnik")
   '   disable the data connection
   MapView1.SetDataConnectionEnabled(False)
   
   If FirstTime Then
      '   center the map within the area covered by the offline tiles
      MapCenter.Initialize(52.75192, 0.40505)
      '   choose a zoom level for which offline tiles exist
      ZoomLevel=10
   End If
   
   MapView1.Zoom=ZoomLevel
   MapView1.SetCenter3(MapCenter)
End Sub

Sub Activity_Resume
End Sub

Sub Activity_Pause (UserClosed As Boolean)
   MapCenter=MapView1.GetCenter
   ZoomLevel=MapView1.Zoom
End Sub

Sub MapView1_ZoomChanged
   If MapView1.Zoom>12 Then
      '   i could have set the map zoom to 12 if it was more than 12 but this way you can see the level 12 tiles stretched to display where zoom level 13+ does not exist
      ToastMessageShow("Tiles are cached for zoom levels 0 to 12 only, current zoom level is: "&MapView1.Zoom, False)
   End If
End Sub

Everything works as desired.
The offline tile cache file is copied to the osmdroid cache folder if it has not already been copied.
And the MapView displays just my offline tiles - where there are no offline tiles the MapView displays just it's background grid.
At zoom levels of 13 and higher the MapView stretches the tiles from zoom level 12 to fit the higher zoom levels.
The MapView1_ZoomChanged event listener could just have easily set the MapView zoom to 12 if it was higher.

There's a few things to watch for with offline tiles:
  • Mobile Atlas Creator does not support downloading of all the TileSources that the MapView supports.
    I see no option to download CycleMap tiles for example.
  • In the MapView the Mapnik tiles cover at least zoom level 18.
    Mobile Atlas Creator offers to download only up to zoom level 16!
  • If offline tiles expire and the MapView will no longer use them then watch that you don't have a massive offline tile archive wasting SD card space.

As well as offline map tiles, we can use a MapView to display any other image tiles.
That makes a MapView an ideal panorama viewer - cut your hi-res panorama image into tiles and view them in glorious hi-res in a MapView.
Circuit diagrams and floorplans are other large images that could be displayed in a MapView without having to worry about the memory overhead that dealing with large bitmaps causes.

Martin.

[edit]Example code updated it now checks if the osmdroid cache folder exists, and creates it if necessary.[/edit]
 

Attachments

  • 11 - Offline mapsUpdated.zip
    319 KB · Views: 3,060
Last edited:

bluedude

Well-Known Member
Licensed User
Longtime User
Hi,

I'm testing your mapview and it is great. One question comes up, how can I use the Cloudmade stuff? I have an API key for Cloudmade. Can I add the API key somewhere?
 

warwound

Expert
Licensed User
Longtime User
I have updated the library to add support for using a Cloudmade API key.

You must add your Cloudmade API key to your manifest.xml file, use Project menu > Manifest Editor and add it like this:

B4X:
'   Add your Cloudmade API key here
AddApplicationText(<meta-data android:name="CLOUDMADE_KEY" android:value="Your Cloudmade API key here" />)

The meta-data element must be a child of the application element and NOT an activity element.

The library update adds a new property to the MapView object: CloudmadeTileSourceStyle.

You can set or get the Cloudmade TileSource Style as an Int using this new property.

Some example code:

B4X:
Sub Process_Globals
   Dim MapCenter As GeoPoint
   Dim ZoomLevel As Int
   
   ' use a process global to save and restore the current Cloudmade TileSource Style
   Dim CloudmadeTileSourceStyle As Int
End Sub

Sub Globals
   '   use a spinner to select the CloudMadeStandardTiles TileSource Style
   Dim CloudmadeTileSourceStyleSelect As Spinner
   Dim MapView1 As MapView
End Sub

Sub Activity_Create(FirstTime As Boolean)
   MapView1.Initialize("")
   Activity.AddView(MapView1, 0, 48dip, 100%x, 100%y-48dip)
   
   MapView1.SetZoomEnabled(True)
   MapView1.SetMultiTouchEnabled(True)
   
   If FirstTime Then
      MapCenter.Initialize(52.75192, 0.40505)
      ZoomLevel=14
      
      ' set the default Cloudmade TileSource Style
      CloudmadeTileSourceStyle=0
   End If
   
   
   '   select the CloudMadeStandardTiles TileSource
   '   an API key is required and should be set in the manifest file
   MapView1.SetTileSource("CloudMadeStandardTiles")
   
   '   set the CloudMadeStandardTiles TileSource Style
   MapView1.CloudmadeTileSourceStyle=CloudmadeTileSourceStyle
   
   MapView1.Zoom=ZoomLevel
   MapView1.SetCenter3(MapCenter)
   
   CloudmadeTileSourceStyleSelect.Initialize("CloudmadeTileSourceStyleSelect")
   Activity.AddView(CloudmadeTileSourceStyleSelect, 0, 0, 100%x, 48dip)
   
   CloudmadeTileSourceStyleSelect.AddAll(Array As String("0", "1", "2", "3", "4", "5", "6", "7", "8"))
   CloudmadeTileSourceStyleSelect.Prompt="CloudmadeTileSourceStyle"
   CloudmadeTileSourceStyleSelect.SelectedIndex=CloudmadeTileSourceStyleSelect.IndexOf(CloudmadeTileSourceStyle)
   
   '   manually call the Spinner ItemClick Sub to sync the CloudMadeStandardTiles TileSource Style with the spinner SelectedIndex
   CloudmadeTileSourceStyleSelect_ItemClick(CloudmadeTileSourceStyleSelect.SelectedIndex, CloudmadeTileSourceStyleSelect.SelectedItem)
   
   
   Log("Activity_Create CloudmadeTileSourceStyle="&CloudmadeTileSourceStyle)
End Sub

Sub Activity_Resume
End Sub

Sub Activity_Pause (UserClosed As Boolean)
   MapCenter=MapView1.GetCenter
   ZoomLevel=MapView1.Zoom
   
   '   save the current Cloudmade TileSource Style
   CloudmadeTileSourceStyle=MapView1.CloudmadeTileSourceStyle
   Log("Activity_Pause CloudmadeTileSourceStyle="&CloudmadeTileSourceStyle)
End Sub

Sub CloudmadeTileSourceStyleSelect_ItemClick (Position As Int, Value As Object)
   MapView1.CloudmadeTileSourceStyle=Value
   '   changes to the Style are only applied when the CloudMade TileSource is (re)loaded
   MapView1.SetTileSource("CloudMadeStandardTiles")
End Sub

Note that changing the Style does not automatically update the MapView.
Changes to the Style are applied when a Cloudmade TileSource is next loaded, so reloading the CloudMadeStandardTiles TileSource actually applies the change.

I've played a bit with the CloudMadeSmallTiles TileSource but found it to behave unpredictably at times.
The MapView panning to (0, 0) when i changed zoom levels sometimes.
OSMDroid natively works with tiles that are 256 pixels square so is probably not properly handling the smaller tiles.

CloudMadeStandardTiles however works fine.

Example code attached - note that i have removed my Cloudmade API key from the example.

Martin.
 

Attachments

  • CloudmadeTiles.zip
    6.5 KB · Views: 1,562

Ohanian

Active Member
Licensed User
Longtime User
tanx for the lib

i have a problem on compiling the sample, i got this error msg :

B4X:
Compiling code.                         0.00
Generating R file.                      0.00
Compiling generated Java code.          Error
B4A line: 23
Activity.AddView(MapView1, 0, 0, 100%x, 100%y)
javac 1.6.0_21
src\uk\co\martinpearman\b4a\mapviewtutorial\main.java:230: cannot access org.osmdroid.views.MapView
class file for org.osmdroid.views.MapView not found
mostCurrent._activity.AddView((android.view.View)(mostCurrent._mapview1.getObject()),(int)(0),(int)(0),anywheresoftware.b4a.keywords.Common.PerXToCurrent((float)(100),mostCurrent.activityBA),anywheresoftware.b4a.keywords.Common.PerYToCurrent((float)(100),mostCurrent.activityBA));
                                                                                 ^
1 error

did i miss something?
 

warwound

Expert
Licensed User
Longtime User
I'd guess that you have not downloaded and copied the two native Android libraries to your B4A additional libraries folder.

This library is OSMDroid.jar and OSMDroid.xml PLUS two additional (native Android) libraries NativeOSMDroid.jar and slf4j-android-1.5.8.jar.

Those last two libraries are not in the B4A library download, you can get them from the main library thread, native_android_libraries.zip is the archive to download.

That archive contains a readme.txt file which explains what the extra files are.

Martin.
 

Ohanian

Active Member
Licensed User
Longtime User
I'd guess that you have not downloaded and copied the two native Android libraries to your B4A additional libraries folder.

This library is OSMDroid.jar and OSMDroid.xml PLUS two additional (native Android) libraries NativeOSMDroid.jar and slf4j-android-1.5.8.jar.

Those last two libraries are not in the B4A library download, you can get them from the main library thread, native_android_libraries.zip is the archive to download.

That archive contains a readme.txt file which explains what the extra files are.

Martin.

Already have these files in the lib folder!
 
Top