Bing mapcontrol Offline Tiles Solution #WPDev #WP7Dev

Please click here to download the source

Lets start by looking at Bing Maps control. We are going to control map loading so we need to ensure that Map.Mode is set to MercatorMode. I have set the TileSource to a custom tile source.

<my:Map.Mode>
    <myCore:MercatorMode></myCore:MercatorMode>
</my:Map.Mode>

<my:MapLayer x:Name="mlTiles">                
    <my:MapTileLayer>
        <my:MapTileLayer.TileSources>
            <locals:BingMapTileSource></locals:BingMapTileSource>
        </my:MapTileLayer.TileSources>
    </my:MapTileLayer>
</my:MapLayer>

now we define tile sources we are going to use. For this example, lets look at bing maps tile source. I have used this mechanism with OpenStreet, OSMRender, Google and Nokia maps as well.

public class BingMapTileSource : TileSource
{
    public BingMapTileSource() : base("http://ecn.t2.tiles.virtualearth.net/tiles/r{quadkey}.png?g=838") { }

    public override System.Uri GetUri(int x, int y, int zoomLevel)
    {
        OfflineTile ot = new OfflineTile(base.GetUri(x, y, zoomLevel), x, y, zoomLevel, TileSourceDef.OpenStreetMaps);
        TileManager.EnqueueRequest(ot);
        return null;
    }
}

As you can see I have a uri for tile source. and I use that to create instance of OfflineTile class and I queue it up.

public enum TileSourceDef
{
    Bing,
    OpenStreetMaps,
}

public class OfflineTile
{
    public string tilePath;
    public Location location;
        
    public bool Ready;

    public OfflineTile(Uri sourceUri, int x, int y, int zoom, TileSourceDef tileSource)
    {
        double lat, lon;
        TileSystem.TileXYToLatLong(x, y, zoom, out lat, out lon);
        location = new Location() { Latitude = lat, Longitude = lon }; 
        IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForApplication();
        string destDir = string.Format("/Offline/{0}", tileSource);
        if (!isf.DirectoryExists(destDir))
            isf.CreateDirectory(destDir);

        string fileName = Path.Combine(destDir, String.Format("{0}.png", TileSystem.TileXYToQuadKey(x, y, zoom)));

        tilePath = fileName;

        if (!isf.FileExists(fileName))
        {
            WebClient wc = new WebClient();
            wc.OpenReadCompleted += (s, ev) =>
            {
                if (ev.Error == null)
                {
                    using (IsolatedStorageFileStream output = isf.OpenFile(fileName, FileMode.OpenOrCreate))
                        ev.Result.CopyTo(output);

                    this.Ready = true;
                }
            };
            wc.OpenReadAsync(sourceUri);
        }
        else
        {
            this.Ready = true;
        }
    }

    public Image GetTileImage()
    {
        IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForApplication();
        BitmapImage bi = new BitmapImage();
            bi.SetSource(isf.OpenFile(this.tilePath, FileMode.Open, FileAccess.Read));

        return new Image() { Source = bi, Height = bi.PixelHeight, Width = bi.PixelWidth };
    }
}

I use another class to translate from X, Y and Z to Latitude and Longitude. There where TileSystem comes into play. I found this in one of the msdn samples and I added just 1 method to it. The method is TileSystem.TileXYToLatLong. In the tile source, the map already passes X, Y. Took me a while to figure out that it was TileX and TileY and that I needed to use Latitude and Longitudes when inserting the tile image. As you can see I used methods already present in the class to do the dirty work :)

static class TileSystem
{
    private const double EarthRadius = 6378137;
    private const double MinLatitude = -85.05112878;
    private const double MaxLatitude = 85.05112878;
    private const double MinLongitude = -180;
    private const double MaxLongitude = 180;


    /// <summary>
    /// Clips a number to the specified minimum and maximum values.
    /// </summary>
    /// <param name="n">The number to clip.</param>
    /// <param name="minValue">Minimum allowable value.</param>
    /// <param name="maxValue">Maximum allowable value.</param>
    /// <returns>The clipped value.</returns>
    private static double Clip(double n, double minValue, double maxValue)
    {
        return Math.Min(Math.Max(n, minValue), maxValue);
    }



    /// <summary>
    /// Determines the map width and height (in pixels) at a specified level
    /// of detail.
    /// </summary>
    /// <param name="levelOfDetail">Level of detail, from 1 (lowest detail)
    /// to 23 (highest detail).</param>
    /// <returns>The map width and height in pixels.</returns>
    public static uint MapSize(int levelOfDetail)
    {
        return (uint)256 << levelOfDetail;
    }



    /// <summary>
    /// Determines the ground resolution (in meters per pixel) at a specified
    /// latitude and level of detail.
    /// </summary>
    /// <param name="latitude">Latitude (in degrees) at which to measure the
    /// ground resolution.</param>
    /// <param name="levelOfDetail">Level of detail, from 1 (lowest detail)
    /// to 23 (highest detail).</param>
    /// <returns>The ground resolution, in meters per pixel.</returns>
    public static double GroundResolution(double latitude, int levelOfDetail)
    {
        latitude = Clip(latitude, MinLatitude, MaxLatitude);
        return Math.Cos(latitude * Math.PI / 180) * 2 * Math.PI * EarthRadius / MapSize(levelOfDetail);
    }



    /// <summary>
    /// Determines the map scale at a specified latitude, level of detail,
    /// and screen resolution.
    /// </summary>
    /// <param name="latitude">Latitude (in degrees) at which to measure the
    /// map scale.</param>
    /// <param name="levelOfDetail">Level of detail, from 1 (lowest detail)
    /// to 23 (highest detail).</param>
    /// <param name="screenDpi">Resolution of the screen, in dots per inch.</param>
    /// <returns>The map scale, expressed as the denominator N of the ratio 1 : N.</returns>
    public static double MapScale(double latitude, int levelOfDetail, int screenDpi)
    {
        return GroundResolution(latitude, levelOfDetail) * screenDpi / 0.0254;
    }



    /// <summary>
    /// Converts a point from latitude/longitude WGS-84 coordinates (in degrees)
    /// into pixel XY coordinates at a specified level of detail.
    /// </summary>
    /// <param name="latitude">Latitude of the point, in degrees.</param>
    /// <param name="longitude">Longitude of the point, in degrees.</param>
    /// <param name="levelOfDetail">Level of detail, from 1 (lowest detail)
    /// to 23 (highest detail).</param>
    /// <param name="pixelX">Output parameter receiving the X coordinate in pixels.</param>
    /// <param name="pixelY">Output parameter receiving the Y coordinate in pixels.</param>
    public static void LatLongToPixelXY(double latitude, double longitude, int levelOfDetail, out int pixelX, out int pixelY)
    {
        latitude = Clip(latitude, MinLatitude, MaxLatitude);
        longitude = Clip(longitude, MinLongitude, MaxLongitude);

        double x = (longitude + 180) / 360;
        double sinLatitude = Math.Sin(latitude * Math.PI / 180);
        double y = 0.5 - Math.Log((1 + sinLatitude) / (1 - sinLatitude)) / (4 * Math.PI);

        uint mapSize = MapSize(levelOfDetail);
        pixelX = (int)Clip(x * mapSize + 0.5, 0, mapSize - 1);
        pixelY = (int)Clip(y * mapSize + 0.5, 0, mapSize - 1);
    }



    /// <summary>
    /// Converts a pixel from pixel XY coordinates at a specified level of detail
    /// into latitude/longitude WGS-84 coordinates (in degrees).
    /// </summary>
    /// <param name="pixelX">X coordinate of the point, in pixels.</param>
    /// <param name="pixelY">Y coordinates of the point, in pixels.</param>
    /// <param name="levelOfDetail">Level of detail, from 1 (lowest detail)
    /// to 23 (highest detail).</param>
    /// <param name="latitude">Output parameter receiving the latitude in degrees.</param>
    /// <param name="longitude">Output parameter receiving the longitude in degrees.</param>
    public static void PixelXYToLatLong(int pixelX, int pixelY, int levelOfDetail, out double latitude, out double longitude)
    {
        double mapSize = MapSize(levelOfDetail);
        double x = (Clip(pixelX, 0, mapSize - 1) / mapSize) - 0.5;
        double y = 0.5 - (Clip(pixelY, 0, mapSize - 1) / mapSize);

        latitude = 90 - 360 * Math.Atan(Math.Exp(-y * 2 * Math.PI)) / Math.PI;
        longitude = 360 * x;
    }



    /// <summary>
    /// Converts pixel XY coordinates into tile XY coordinates of the tile containing
    /// the specified pixel.
    /// </summary>
    /// <param name="pixelX">Pixel X coordinate.</param>
    /// <param name="pixelY">Pixel Y coordinate.</param>
    /// <param name="tileX">Output parameter receiving the tile X coordinate.</param>
    /// <param name="tileY">Output parameter receiving the tile Y coordinate.</param>
    public static void PixelXYToTileXY(int pixelX, int pixelY, out int tileX, out int tileY)
    {
        tileX = pixelX / 256;
        tileY = pixelY / 256;
    }



    /// <summary>
    /// Converts tile XY coordinates into pixel XY coordinates of the upper-left pixel
    /// of the specified tile.
    /// </summary>
    /// <param name="tileX">Tile X coordinate.</param>
    /// <param name="tileY">Tile Y coordinate.</param>
    /// <param name="pixelX">Output parameter receiving the pixel X coordinate.</param>
    /// <param name="pixelY">Output parameter receiving the pixel Y coordinate.</param>
    public static void TileXYToPixelXY(int tileX, int tileY, out int pixelX, out int pixelY)
    {
        pixelX = tileX * 256;
        pixelY = tileY * 256;
    }

    public static void TileXYToLatLong(int tileX, int tileY, int zoomLevel, out double latitude, out double longitude)
    {
        int pixelX;
        int pixelY;

        TileXYToPixelXY(tileX, tileY, out pixelX, out pixelY);

        PixelXYToLatLong(pixelX, pixelY, zoomLevel, out latitude, out longitude);
    }

    /// <summary>
    /// Converts tile XY coordinates into a QuadKey at a specified level of detail.
    /// </summary>
    /// <param name="tileX">Tile X coordinate.</param>
    /// <param name="tileY">Tile Y coordinate.</param>
    /// <param name="levelOfDetail">Level of detail, from 1 (lowest detail)
    /// to 23 (highest detail).</param>
    /// <returns>A string containing the QuadKey.</returns>
    public static string TileXYToQuadKey(int tileX, int tileY, int levelOfDetail)
    {
        StringBuilder quadKey = new StringBuilder();
        for (int i = levelOfDetail; i > 0; i--)
        {
            char digit = '0';
            int mask = 1 << (i - 1);
            if ((tileX & mask) != 0)
            {
                digit++;
            }
            if ((tileY & mask) != 0)
            {
                digit++;
                digit++;
            }
            quadKey.Append(digit);
        }
        return quadKey.ToString();
    }

        
    /// <summary>
    /// Converts a QuadKey into tile XY coordinates.
    /// </summary>
    /// <param name="quadKey">QuadKey of the tile.</param>
    /// <param name="tileX">Output parameter receiving the tile X coordinate.</param>
    /// <param name="tileY">Output parameter receiving the tile Y coordinate.</param>
    /// <param name="levelOfDetail">Output parameter receiving the level of detail.</param>
    public static void QuadKeyToTileXY(string quadKey, out int tileX, out int tileY, out int levelOfDetail)
    {
        tileX = tileY = 0;
        levelOfDetail = quadKey.Length;
        for (int i = levelOfDetail; i > 0; i--)
        {
            int mask = 1 << (i - 1);
            switch (quadKey[levelOfDetail - i])
            {
                case '0':
                    break;

                case '1':
                    tileX |= mask;
                    break;

                case '2':
                    tileY |= mask;
                    break;

                case '3':
                    tileX |= mask;
                    tileY |= mask;
                    break;

                default:
                    throw new ArgumentException("Invalid QuadKey digit sequence.");
            }
        }
 \   }
}

Finally lets see what actually happens to the queued tile. All it does is spawn off the request using ThreadPool

public class TileManager
{
    static Queue<OfflineTile> requestQueue = new Queue<OfflineTile>();
    public static Queue<OfflineTile> ResponseQueue = new Queue<OfflineTile>();

    static ManualResetEvent waitEvent;

    static volatile bool WaitWhileProcessing = false;

    public static void Initialise(ManualResetEvent mreWait)
    {
        waitEvent = mreWait;
    }

    public static void EnqueueRequest(OfflineTile ot)
    {
        requestQueue.Enqueue(ot);

        if (!WaitWhileProcessing)
        {
            WaitWhileProcessing = true;
            ThreadPool.QueueUserWorkItem(ProcessTiles);
        }
    }

    public static void ProcessTiles(Object stateInfo)
    {
        waitEvent.Set();
        while (requestQueue.Count > 0)
        {
            OfflineTile t = requestQueue.Peek();
            if (t.Ready)
            {
                t = requestQueue.Dequeue();

                //Dispatcher.BeginInvoke(() => mapLayer.AddChild(t.TileBitmap, new System.Device.Location.GeoCoordinate(t.y, t.X)));
                ResponseQueue.Enqueue(t);
            }
            else
                Thread.Sleep(10);
        }

        waitEvent.Reset();
        WaitWhileProcessing = false;
    }
}

Now in the code behind this is what I do. I spawn off another thread that monitors for any available tiles and adds them to UI.

ManualResetEvent mrt = new ManualResetEvent(false);
Thread t = null;

private void ManageMapTiles()
{
    while (true)
    {
        App.WaitEvent.WaitOne(10);

        if (TileManager.ResponseQueue.Count > 0)
        {
            OfflineTile ot = TileManager.ResponseQueue.Dequeue();
            Dispatcher.BeginInvoke(() => this.SetTile(ot));
        }
    }
}

private object lockobj = new object();

private void SetTile(OfflineTile ot)
{
    lock (lockobj)
    {
        IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForApplication();
        BitmapImage bi = new BitmapImage();
        bi.SetSource(isf.OpenFile(ot.tilePath, FileMode.Open, FileAccess.Read));

        Image img = new Image() { Source = bi, Height = bi.PixelHeight, Width = bi.PixelWidth, Opacity = 1, Stretch = System.Windows.Media.Stretch.None };
        mlTiles.AddChild(img, new GeoCoordinate(ot.location.Latitude, ot.location.Longitude));
    }
}
private void PhoneApplicationPage_Loaded(object sender, RoutedEventArgs e)
{
    t = new Thread(new ThreadStart(ManageMapTiles));
    t.Start();

    watcher = new GeoCoordinateWatcher(GeoPositionAccuracy.High);
    watcher.MovementThreshold = 10;

    watcher.StatusChanged += new EventHandler<GeoPositionStatusChangedEventArgs>(watcher_StatusChanged);

    watcher.PositionChanged += new EventHandler<GeoPositionChangedEventArgs<GeoCoordinate>>(watcher_PositionChanged);

    // start up LocServ in bg; watcher_StatusChanged will be called when complete.
    new Thread(startLocServInBackground).Start();
}

now let me just tell you there is much optimisation that can be done and I did for GoA2B..its a shame I never released it. Instead of just one tile layer, you need to have multiple map layers one for each zoom levels and you show / hide desired (depending upon zoom) the correct layer.

Been keeping busy with #windowsphone #wp7dev

Go a2b still in beta at version 0.5
I have been spending a bit of time on Cool Camera. Version 1.5 is going to be out soon. Version 1.6 is almost ready. I have to start localisation soon.
So far new features in 1.6 include
* HSL (Hue, Saturation and luminance) Dynamic filter
* YUV (Actually YCbCr) – Brightness, Blue and Red chroma dynamic filter
* Corrected preview of Pixelate & Hexagoneal pixelate
* Upload of photos and videos to SkyDrive
* Since album for both photos and videos.

I will post some code on SkyDrive soon

Go a2b progress #wp7 #windowsphone #wp7dev

Every couple of weeks i usually come back to Go a2b. I recently realised that it wasn’t tethering that was causing me to exceed my mobile data limits. It was the fact that they had not put me on the right package :) So while commuting in the traing, i have started testing Go a2b again.

I got the course based map turning correctly. I was so happy that i submitted it for beta. Then last night i had an epiphany and i got the zoom issue sorted. Now you can zoom in and out without any issues. no image corruption / lag any more. However i screwed up the map rotation :( i guess i have to check it again and go for a drive later on.

I love windowsphone. Its almost been a year and this i guess is the biggest app. Currently support maps are Bing, OpenStreet, Osmarender and Google maps. I tried yahoo but i had issues, I am looking into Nokia maps.

Go a2b inching closer to beta 1 #wp7dev #windowsphone #wp7

I have spent a few more hours on Go a2b and things have progressed further. Routing is working. I have got map rotating correctly. There is an issue with zooming in and out but i am looking into it.

Currently supported maps are Bing and OpenStreetMaps. Gonig to add Google and Yahoo before beta 1

After beta 1, i plan to get TTS and HUD working correctly.

please contact me if you wish to take part in the beta and help me get it right.

Cool Camera 1.1 and Go A2B

Cool Camera 1.1 is out now.. enhancements include

1) 5-step digital zoom

2) pinch zoom, drag and flick pictures in picture viewer.

3) live effects preview on thumbnails

4) localisation to all support languages – had to eat my words after checking stats for other apps.

5) layout simplification- two columns of selectable options instead of rows.

You can download the update by clicking the link below.

Go A2B works has been slow and i have slipped my target. I am still doing the HUD and i am trying to figure out how to calculate bits of display items e.g. speed – thats simple, distance pending on current leg, next itinreray (for displaying additional directions), max speed for this itinerary, ETA.

I spent about a day debating whether i should go with streamed TTS or have a nice sound files. I guess v1.0 will feature native sound files for English and i will expand in different markets once i have localised sound files in those languages.

Cool Camera 1.1 submitted

I had a last minute niggle yesterday… i finished localisation and the application of filters to thumbnail, obfuscated the xap and was ready to submit it and i thought… i should test and and guess what happened !!!

1) the zoom changes (2nd part – using ScaleTransform) mean the image was getting larger and going under the zoom buttons.. which of course was not acceptable.

2) same thing was happening in picture viewer.

so i spent time today to fix that, reverted to cropping the image and fitting it to image rather than scaling the image (i think its slower than scaling slightly as there’s bitmap creation etc but it works… we shall see.

the only thing i would have liked further is kinetic scrolling on flicking a zoom image, currently i move it twice the distance based on velocity.

Now i am back on Go A2B, the car has been moved to the center, the map viewer and i am working on checking the current location against the path and to recalculate if it moves say 50 meters off the current path., i have started to adding things to the HUD.

Go A2B – Route Calculation complete

Day 5 today: I thought i wouldn’t get much done on Sunday but i was wrong. I did spend an hour or two yesterday and i worked on the map display side of things.

I added the offline tiles code and i optimsed the code to use a single thread that runs / stops as required – using my favourite ManualResetEvent to pause / restart / exit it. In fact its good code that does what it needs to.

Today i got the route calculation part in and i plugged it in with the GeoWatcher so once current location is determined, route calculation is called. BTW i did get around to doing a physical phone test today!!

Next things so do

1) Speed, next leg info etc in the hud

2) TTS based on route info

3) get the bleeding car in the centre of the map

4) rotate the map as required based on values returned by gps.

5) do settings page

For anyone interested in doing something like this, don’t persist GeoResult – i tried and i got a few FormatException trying to deserialise it. Now i am persisting way point.