Article written

  • on 14.07.2011
  • at 12:31 PM
  • by Denis

Custom map application for Windows Phone 7 0

Develop Custom Map application for Windows Phone 7.
The requirement for the application:
1.    Show one layer of the map (100x100 tiles), which was received from the internet using HTTP protocol. The size of one tile is 128x128 pixels.
2.    Moving the map around by dragging it – either with your finger or the mouse (movement should be smooth).
3.    Zooming(scaling) a layer using gestures.
4.    Application should use cache of the tiles (Isolated Storage).  Once tile was loaded it shouldn't be required again.
5.    The application must not use more than 90 megabytes of RAM and 90 MB of video memory.

As it is, the task seems real and reasonable.

For the main map component I decided to use MultiScaleImage component which seems to provide all necessary features. For the map API I am using Yandex Map API (in particular Static API) (to read more about Yandex Map Static API follow the link). I don’t have real device on the Windows Phone 7 platform so I have to split event handler for the multi touch and testing on the computer. For this reason I chose Laurent Bugnion’s Multi Touch Behaviour component which seems very suitable for me.

Tile sources

In order to provide load custom maps I had to create class which inherits from the MultiScaleTileSource. This class sports an abstract method GetTileLayers(int tileLevel, int tilePositionX, int tilePositionY, IList<object>tileImageLayerSources) that we can override. Each descendant essentially tells the MultiScaleImage: if you are on tileLevel, and need to have the tilePositionX horizontal tile and the tilePositionY vertical tile, then I will add an URI to the tileImageLayerSources where you can find it. So class YandexTileSource is simply a translation from what the MultiScaleImage asks into URIs of actual map images on various map servers on the internet. To learn more about tile system follow the link - Bing Maps Tile System.
The map image size and tile size  need to be specified in the constructor of the base MultiScaleTileSource class. So the basic Idea of the YandexTileSource class is shown below:

public class YandexTileSource : MultiScaleTileSource
{
	public YandexTileSource()
		: base(0x1000000, 0x1000000, 0x80, 0x80, 0)
	{
	}

	public virtual int TileToZoom(int tileLevelDetail)
	{
		return tileLevelDetail - 8;
	}

	public string UriFormat
	{
		get { return @"http://static-maps.yandex.ru/1.x/?ll={0},{1}&z={2}&size={3},{4}&l={5}&key=ANrhFk4BAAAAL5FwCwIA8zlN0fHLZn82j09mMxedkvBX7Z4AAAAAAAAAAABaczXjSkSnHzxjBc1YuaJ6lC1qAQ=="; }
	}

	protected override void GetTileLayers(int tileLevel, int tilePositionX, int tilePositionY, IList<object> tileImageLayerSources)
	{
		System.Diagnostics.Debug.WriteLine("Level: " + tileLevel + " PositionX: " + tilePositionX + " PositionY " + tilePositionY);
		var zoom = TileToZoom(tileLevel);

		int size = 128;

		if (zoom > 0)
		{
			double longitude = 0;
			double latitude = 0;

			TileSystem.PixelXYToLatLong(tilePositionX * size, tilePositionY * size, zoom, out latitude, out longitude);

			var url = string.Format(UriFormat, longitude, latitude, zoom, size, size, "map");
			var veUri = new Uri(url);
			System.Diagnostics.Debug.WriteLine("adding uri " + url);
			tileImageLayerSources.Add(veUri);
		}
	}
}

For the transformation tilePositionX and tilePositionY coordinates I using TileSystem class with PixelXYToLatLong method.  The source code of this class you can find on the link.
The algorithms for loading tiles from the Google, Bing servers and OSM almost the same.

So with Title system we done, now let’s have a look on the event handler system.

Events Behavior

As I said earlier I was going to create a class for handling movement events. The first MouseClickBehavior is responsible for zooming by mouse clicks (or by finger touch) and another MotionBehavior for Multi Touch. Source code for this classes can be found below:

public class MotionBehavior : Behavior<MultiScaleImage>
{

	public MultiScaleImage multiScaleImage  { get { return AssociatedObject; } }
	private Point manipulationOrigin        { get; set; }
	private Point multiScaleImageOrigin     { get; set; }
	private double currentZoom = 1;

	/// <summary>
	/// Attach event handler
	/// </summary>
	protected override void OnAttached()
	{
		base.OnAttached();
		AssociatedObject.ManipulationStarted += AssociatedObject_ManipulationStarted;
		AssociatedObject.ManipulationDelta += AssociatedObject_ManipulationDelta;           
	}

	void AssociatedObject_ManipulationStarted(object sender, ManipulationStartedEventArgs e)
	{
		multiScaleImageOrigin   = new Point(multiScaleImage.ViewportOrigin.X, multiScaleImage.ViewportOrigin.Y);
		manipulationOrigin      = e.ManipulationOrigin;
	}

	void AssociatedObject_ManipulationDelta(object sender, ManipulationDeltaEventArgs e)
	{
		if (e.DeltaManipulation.Scale.X == 0 && e.DeltaManipulation.Scale.Y == 0)
		{                
			multiScaleImage.ViewportOrigin = new Point
											{
												X = multiScaleImageOrigin.X -
													(e.CumulativeManipulation.Translation.X /
													multiScaleImage.ActualWidth * multiScaleImage.ViewportWidth),
												Y = multiScaleImageOrigin.Y -
													(e.CumulativeManipulation.Translation.Y /
													multiScaleImage.ActualHeight * multiScaleImage.ViewportWidth),
											};
		}
		else
		{
			var zoomScale = (e.DeltaManipulation.Scale.X + e.DeltaManipulation.Scale.Y) / 2;

			var logicalPoint = multiScaleImage.ElementToLogicalPoint(new Point
					{
						X = manipulationOrigin.X - e.CumulativeManipulation.Translation.X,
						Y = manipulationOrigin.Y - e.CumulativeManipulation.Translation.Y
					}
				);
			multiScaleImage.ZoomAboutLogicalPoint(zoomScale, logicalPoint.X, logicalPoint.Y);
			currentZoom = zoomScale;

			if (multiScaleImage.ViewportWidth > 1) 
				multiScaleImage.ViewportWidth = 1;
		}
	}

	public void ZoomIn()
	{
		Zoom(currentZoom * MultiScaleImageConstants.zoomInFactor, multiScaleImage.ElementToLogicalPoint(new Point(.5 * multiScaleImage.ActualWidth, .5 * multiScaleImage.ActualHeight)));
	}

	public void ZoomOut()
	{
		Zoom(currentZoom * MultiScaleImageConstants.zoomOutFactor, multiScaleImage.ElementToLogicalPoint(new Point(.5 * multiScaleImage.ActualWidth, .5 * multiScaleImage.ActualHeight)));
	}

	public void SetViewport(double width, Point point)
	{
		multiScaleImage.ViewportWidth = width;
		multiScaleImage.ViewportOrigin = point;
	}

	private void Zoom(double zoom, Point point)
	{
		multiScaleImage.ZoomAboutLogicalPoint(zoom / currentZoom, point.X, point.Y);
		currentZoom = zoom;
	}

	/// <summary>
	/// Detach event handler
	/// </summary>
	protected override void OnDetaching()
	{
		AssociatedObject.ManipulationStarted    -= AssociatedObject_ManipulationStarted;
		AssociatedObject.ManipulationDelta      -= AssociatedObject_ManipulationDelta;
		base.OnDetaching();
	}
}

public class MouseClickBehavior : Behavior<MultiScaleImage>
{
	private double currentZoom  = 1;
	private bool isDrag         = false;
	private bool isMouseDown    = false;

	private Point lastMouseDownPoint;
	private Point lastMousePoint;
	private Point lastViewPort;

	protected override void OnAttached()
	{
		base.OnAttached();
		AssociatedObject.MouseLeftButtonDown    += new MouseButtonEventHandler(AssociatedObject_MouseLeftButtonDown);
		AssociatedObject.MouseLeftButtonUp      += new MouseButtonEventHandler(AssociatedObject_MouseLeftButtonUp);
		AssociatedObject.MouseMove              += new MouseEventHandler(AssociatedObject_MouseMove);
	}

	public MultiScaleImage multiScaleImage { get { return AssociatedObject; } }

	void AssociatedObject_MouseMove(object sender, MouseEventArgs e)
	{
		lastMousePoint = e.GetPosition(multiScaleImage);

		if (isMouseDown && !isDrag)
		{
			isDrag = true;
			multiScaleImage.ViewportOrigin = new Point(multiScaleImage.ViewportOrigin.X, multiScaleImage.ViewportOrigin.Y);
		}

		if (isDrag)
		{
			Point newPoint = lastViewPort;
			newPoint.X += (lastMouseDownPoint.X - lastMousePoint.X) / multiScaleImage.ActualWidth * multiScaleImage.ViewportWidth;
			newPoint.Y += (lastMouseDownPoint.Y - lastMousePoint.Y) / multiScaleImage.ActualWidth * multiScaleImage.ViewportWidth;
			multiScaleImage.ViewportOrigin = newPoint;
		}
	}

	void AssociatedObject_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
	{
		if (!isDrag)
			Zoom(currentZoom * MultiScaleImageConstants.zoomInFactor, multiScaleImage.ElementToLogicalPoint(e.GetPosition(multiScaleImage)));

		isDrag      = false;
		isMouseDown = false;
	}

	void AssociatedObject_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
	{
		lastMouseDownPoint  = e.GetPosition(multiScaleImage);
		lastViewPort        = multiScaleImage.ViewportOrigin;

		isMouseDown = true;
	}

	private void Zoom(double zoom, Point point)
	{
		multiScaleImage.ZoomAboutLogicalPoint(zoom / currentZoom, point.X, point.Y);
		currentZoom = zoom;
	}

	public void ZoomIn()
	{
		Zoom(currentZoom * MultiScaleImageConstants.zoomInFactor, multiScaleImage.ElementToLogicalPoint(new Point(.5 * multiScaleImage.ActualWidth, .5 * multiScaleImage.ActualHeight)));
	}

	public void ZoomOut()
	{
		Zoom(currentZoom * MultiScaleImageConstants.zoomOutFactor, multiScaleImage.ElementToLogicalPoint(new Point(.5 * multiScaleImage.ActualWidth, .5 * multiScaleImage.ActualHeight)));
	}

	public void SetViewport(double width, Point point)
	{
		multiScaleImage.ViewportWidth   = width;
		multiScaleImage.ViewportOrigin  = point;
	}

	protected override void OnDetaching()
	{
		AssociatedObject.MouseLeftButtonDown    -= new MouseButtonEventHandler(AssociatedObject_MouseLeftButtonDown);
		AssociatedObject.MouseLeftButtonUp      -= new MouseButtonEventHandler(AssociatedObject_MouseLeftButtonUp);
		AssociatedObject.MouseMove              -= new MouseEventHandler(AssociatedObject_MouseMove);
		base.OnDetaching();
	}
}

 

Now we just need to set  the source for the MultiScaleImage and add behaviors. It will be like this:

multiScaleImage.Source = new YandexMaps.DAL.YandexTileSource();

behaviorCollection = Interaction.GetBehaviors(multiScaleImage);
behaviorCollection.Add(new MouseClickBehavior());

XAML code for the main page:

<Grid x:Name="LayoutRoot" Background="Transparent">
   <MultiScaleImage Name="multiScaleImage" />
</Grid>

Problem

Everything looks very good, except one thing - this application does not fit the requirement number 4 - application should use cache of the tiles. And this is a real problem and pain in the a**. I spent a lot of time trying to change the tileImageLayerSources URI to the local directory but it didn’t work. Also I found that MultiScaleImage couldn't work with local resources at all!!!??? So, if anyone has the Idea how to make it work please let me know.

I'm going to write special article on this issue. Currently I see only 2 approaches:

1.    Write a local webserver on the windows phone to handle http requests. This web server can run the application and cache the images.
2.    Create a new MultiScaleImage component.

The source code for the current version of application  you can download from here.

I would very much appreciate feedback on  how the thing work in real life.

blog comments powered by Disqus

Denis Blog is powered by Drupal
developed by Denis Liger

Valid XHTML 1.0 Strict

Рейтинг@Mail.ru