Xamarin.Android maps – adding high-resolution map images

Recently I started to build a map-based app where I wanted to show a map with map images as an overlay. For example, if I have an image for a specific area I want to show it on the map in the app.

The way to do that is to use GroundOverlayOptions. and just use the image and the bounding box for the image.

var description = BitmapDescriptorFactory.FromBitmap(bitmap);
var overlay = new GroundOverlayOptions();
overlay.PositionFromBounds(new LatLngBounds(new LatLng(south,west), new LatLng(north,east)));

Ok, that was pretty simple. But my problem was that the image I wanted to use was very in very high resolution (about 5000 x 9000 px). If you have developed Android app for a while you probably know that Android is no so good at handling high-resolution images. This is not a problem related to Xamarin, it is in the Android operating system. You often get out of memory exception or as in my case the BitmapFactory just returned null when I tried to decode the image stream.

One solution could be to just compress the image by setting InSampleSize. Setting InSampleSize to 2 will make the image half the size, and setting it to 4 will make it four times smaller and so on.

var options = new BitmapFactory.Options();
options.InSampleSize = 2;
var bitmap = BitmapFactory.DecodeStream(stream,null, options);

I had to set InSampleSize to 4 to get it to work, but the problem then was that the I could no longer read the text in the image because of lower quality.

What I had to do was to splice the image into smaller pieces. Android will handle memory better for multiple smaller images than one big. To read just a specific area of an image into memory we can use BitmapRegionDecoder. But first, we need to know the dimension of the full image so we can calculate how the dimension for one piece should be. To do that we use the regular BitmapFactory, but we send in an options object with InJustDecodeBounds set to true. It means it will just read the bounds of the image, then we can access height and width by the OutHeight and OutWidth of the options object.

var options = new BitmapFactory.Options();
options.InJustDecodeBounds = true;
BitmapFactory.DecodeStream(stream,null, options);
//How many rows and columns the original images should be split in. Set it 4 makes it splice 16 small images
var split = 4;
//Calculate the width and height for one image
var width = options.OutWidth / split;
var height = options.OutHeight / split;
var regionDecoder = BitmapRegionDecoder.NewInstance(stream, true);

Now when we now the width and height of one piece we need to split the original image and put them together in a 4x4 grid and overlay them on the map.

var positionX = 0;
var positionY = 0;
//Calculate the distance for one image slice.
 var lng =  (east - west)/split;
var lat = (north - south) / split;
var opt = new BitmapFactory.Options();
//Loop through all columns and rows to add images/overlays to the map
for (var x = 0; x < split; x++)
     positionX = width * x;
     var w = west + lng * x;
     var e = w + lng;
     for (var y = 0; y < split; y++)
          positionY = height * y;
          var n = north - lat * y;
          var s = n - lat;
          //Get the bitmap for this image piece 
          var bitmap = regionDecoder.DecodeRegion(new Android.Graphics.Rect(positionX, positionY, positionX+width, positionY+height), opt);
          var description = BitmapDescriptorFactory.FromBitmap(bitmap);
          var overlay = new GroundOverlayOptions().PositionFromBounds(new LatLngBounds(new LatLng(s,w), new LatLng(n,e)));